ucpify 1.0.0 → 1.1.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/dist/cli.js +3 -2
- package/dist/express-app.js +12 -10
- package/dist/logger.js +1 -1
- package/dist/ucp-server.d.ts +2 -0
- package/dist/ucp-server.js +56 -14
- package/package.json +10 -5
- package/src/cli.ts +3 -2
- package/src/express-app.ts +25 -22
- package/src/logger.ts +1 -1
- package/src/ucp-server.ts +84 -44
- package/vitest.config.ts +7 -0
package/dist/cli.js
CHANGED
|
@@ -40,10 +40,11 @@ const path = __importStar(require("path"));
|
|
|
40
40
|
const schema_1 = require("./schema");
|
|
41
41
|
const express_app_1 = require("./express-app");
|
|
42
42
|
const program = new commander_1.Command();
|
|
43
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
43
44
|
program
|
|
44
|
-
.name('
|
|
45
|
+
.name('ucpify')
|
|
45
46
|
.description('Generate and run a UCP-compliant server for merchants')
|
|
46
|
-
.version(
|
|
47
|
+
.version(pkg.version);
|
|
47
48
|
program
|
|
48
49
|
.command('init')
|
|
49
50
|
.description('Create a sample merchant configuration file')
|
package/dist/express-app.js
CHANGED
|
@@ -278,6 +278,16 @@ function createExpressApp(config, options) {
|
|
|
278
278
|
});
|
|
279
279
|
// Admin dashboard HTML
|
|
280
280
|
app.get('/admin', (_req, res) => {
|
|
281
|
+
const statsScript = useDb
|
|
282
|
+
? `fetch('/admin/stats').then(r=>r.json()).then(d=>{
|
|
283
|
+
document.getElementById('stats').innerHTML = \`
|
|
284
|
+
<div class="card"><div class="stat">\${Object.values(d.checkouts).reduce((a,b)=>a+b,0)}</div><div class="label">Total Checkouts</div></div>
|
|
285
|
+
<div class="card"><div class="stat">\${d.checkouts.completed||0}</div><div class="label">Completed</div></div>
|
|
286
|
+
<div class="card"><div class="stat">\${d.orders.today||0}</div><div class="label">Orders Today</div></div>
|
|
287
|
+
<div class="card"><div class="stat">$\${(d.revenue.total_succeeded/100).toFixed(2)}</div><div class="label">Revenue</div></div>
|
|
288
|
+
\`;
|
|
289
|
+
}).catch(e=>document.getElementById('stats').innerHTML='Error loading stats');`
|
|
290
|
+
: `document.getElementById('stats').innerHTML = '<div class="card" style="grid-column:1/-1"><div class="label">Running in <strong>in-memory mode</strong>. Stats require <code>--db</code> mode.</div><div style="margin-top:8px;font-size:0.85em;color:#999">Restart with: <code>ucpify serve config.json</code> (DB is on by default)</div></div>';`;
|
|
281
291
|
res.send(`<!DOCTYPE html>
|
|
282
292
|
<html><head><title>UCP Admin</title>
|
|
283
293
|
<style>
|
|
@@ -287,20 +297,12 @@ function createExpressApp(config, options) {
|
|
|
287
297
|
.label { color: #666; font-size: 0.9em; }
|
|
288
298
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }
|
|
289
299
|
h1 { color: #333; }
|
|
300
|
+
code { background: #e5e5e5; padding: 2px 6px; border-radius: 3px; font-size: 0.85em; }
|
|
290
301
|
</style>
|
|
291
302
|
</head><body>
|
|
292
303
|
<h1>🛒 UCP Server Admin</h1>
|
|
293
304
|
<div class="grid" id="stats">Loading...</div>
|
|
294
|
-
<script>
|
|
295
|
-
fetch('/admin/stats').then(r=>r.json()).then(d=>{
|
|
296
|
-
document.getElementById('stats').innerHTML = \`
|
|
297
|
-
<div class="card"><div class="stat">\${Object.values(d.checkouts).reduce((a,b)=>a+b,0)}</div><div class="label">Total Checkouts</div></div>
|
|
298
|
-
<div class="card"><div class="stat">\${d.checkouts.completed||0}</div><div class="label">Completed</div></div>
|
|
299
|
-
<div class="card"><div class="stat">\${d.orders.today||0}</div><div class="label">Orders Today</div></div>
|
|
300
|
-
<div class="card"><div class="stat">$\${(d.revenue.total_succeeded/100).toFixed(2)}</div><div class="label">Revenue</div></div>
|
|
301
|
-
\`;
|
|
302
|
-
}).catch(e=>document.getElementById('stats').innerHTML='Error loading stats');
|
|
303
|
-
</script>
|
|
305
|
+
<script>${statsScript}</script>
|
|
304
306
|
</body></html>`);
|
|
305
307
|
});
|
|
306
308
|
// Health check
|
package/dist/logger.js
CHANGED
package/dist/ucp-server.d.ts
CHANGED
package/dist/ucp-server.js
CHANGED
|
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.UCPServer = void 0;
|
|
4
4
|
const uuid_1 = require("uuid");
|
|
5
5
|
const schema_1 = require("./schema");
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
6
|
+
const stripe_1 = require("./stripe");
|
|
7
|
+
const paypal_1 = require("./paypal");
|
|
8
|
+
const logger_1 = require("./logger");
|
|
9
9
|
class UCPServer {
|
|
10
10
|
config;
|
|
11
11
|
itemsMap;
|
|
12
|
+
checkoutSessions = new Map();
|
|
13
|
+
orders = new Map();
|
|
12
14
|
constructor(config) {
|
|
13
15
|
this.config = config;
|
|
14
16
|
this.itemsMap = new Map(config.items.map((item) => [item.id, item]));
|
|
@@ -156,16 +158,16 @@ class UCPServer {
|
|
|
156
158
|
continue_url: `${this.config.domain}/checkout/${id}`,
|
|
157
159
|
expires_at: expiresAt,
|
|
158
160
|
};
|
|
159
|
-
checkoutSessions.set(id, session);
|
|
161
|
+
this.checkoutSessions.set(id, session);
|
|
160
162
|
return session;
|
|
161
163
|
}
|
|
162
164
|
// Get checkout session
|
|
163
165
|
getCheckout(id) {
|
|
164
|
-
return checkoutSessions.get(id);
|
|
166
|
+
return this.checkoutSessions.get(id);
|
|
165
167
|
}
|
|
166
168
|
// Update checkout session
|
|
167
169
|
updateCheckout(id, data) {
|
|
168
|
-
const session = checkoutSessions.get(id);
|
|
170
|
+
const session = this.checkoutSessions.get(id);
|
|
169
171
|
if (!session)
|
|
170
172
|
return undefined;
|
|
171
173
|
// Update buyer
|
|
@@ -251,18 +253,58 @@ class UCPServer {
|
|
|
251
253
|
const newMessages = this.validateCheckout(session);
|
|
252
254
|
session.messages = newMessages.length > 0 ? newMessages : undefined;
|
|
253
255
|
session.status = newMessages.length > 0 ? 'incomplete' : 'ready_for_complete';
|
|
254
|
-
checkoutSessions.set(id, session);
|
|
256
|
+
this.checkoutSessions.set(id, session);
|
|
255
257
|
return session;
|
|
256
258
|
}
|
|
257
259
|
// Complete checkout (create order)
|
|
258
260
|
async completeCheckout(id) {
|
|
259
|
-
const session = checkoutSessions.get(id);
|
|
261
|
+
const session = this.checkoutSessions.get(id);
|
|
260
262
|
if (!session)
|
|
261
263
|
return { error: 'Checkout session not found' };
|
|
262
264
|
if (session.status !== 'ready_for_complete') {
|
|
263
265
|
return { error: 'Checkout is not ready for completion', messages: session.messages };
|
|
264
266
|
}
|
|
265
267
|
const orderId = `order_${(0, uuid_1.v4)().replace(/-/g, '').slice(0, 12)}`;
|
|
268
|
+
// Get total amount for payment
|
|
269
|
+
const totalAmount = session.totals.find(t => t.type === 'total')?.amount || 0;
|
|
270
|
+
// Payment processing - try Stripe first, then PayPal
|
|
271
|
+
if (totalAmount > 0) {
|
|
272
|
+
if ((0, stripe_1.isStripeConfigured)()) {
|
|
273
|
+
try {
|
|
274
|
+
const paymentIntent = await (0, stripe_1.createPaymentIntent)({
|
|
275
|
+
amount: totalAmount,
|
|
276
|
+
currency: session.currency,
|
|
277
|
+
checkoutId: id,
|
|
278
|
+
customerEmail: session.buyer?.email,
|
|
279
|
+
metadata: { order_id: orderId },
|
|
280
|
+
});
|
|
281
|
+
if (paymentIntent) {
|
|
282
|
+
logger_1.logger.info({ checkoutId: id, paymentIntentId: paymentIntent.id, amount: totalAmount }, 'Stripe payment intent created');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
logger_1.logger.error({ error, checkoutId: id }, 'Failed to create Stripe payment intent');
|
|
287
|
+
return { error: 'Payment processing failed' };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else if ((0, paypal_1.isPayPalConfigured)()) {
|
|
291
|
+
try {
|
|
292
|
+
const ppOrder = await (0, paypal_1.createPayPalOrder)({
|
|
293
|
+
amount: totalAmount,
|
|
294
|
+
currency: session.currency,
|
|
295
|
+
checkoutId: id,
|
|
296
|
+
description: `Order ${orderId}`,
|
|
297
|
+
});
|
|
298
|
+
if (ppOrder) {
|
|
299
|
+
logger_1.logger.info({ checkoutId: id, paypalOrderId: ppOrder.id, amount: totalAmount }, 'PayPal order created');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
logger_1.logger.error({ error, checkoutId: id }, 'Failed to create PayPal order');
|
|
304
|
+
return { error: 'Payment processing failed' };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
266
308
|
const order = {
|
|
267
309
|
ucp: {
|
|
268
310
|
version: schema_1.UCP_VERSION,
|
|
@@ -294,7 +336,7 @@ class UCPServer {
|
|
|
294
336
|
: undefined,
|
|
295
337
|
created_at: new Date().toISOString(),
|
|
296
338
|
};
|
|
297
|
-
orders.set(orderId, order);
|
|
339
|
+
this.orders.set(orderId, order);
|
|
298
340
|
// Update session to completed state with order confirmation
|
|
299
341
|
session.status = 'completed';
|
|
300
342
|
session.order = {
|
|
@@ -303,26 +345,26 @@ class UCPServer {
|
|
|
303
345
|
};
|
|
304
346
|
delete session.continue_url;
|
|
305
347
|
delete session.messages;
|
|
306
|
-
checkoutSessions.set(id, session);
|
|
348
|
+
this.checkoutSessions.set(id, session);
|
|
307
349
|
return session;
|
|
308
350
|
}
|
|
309
351
|
// Cancel checkout
|
|
310
352
|
cancelCheckout(id) {
|
|
311
|
-
const session = checkoutSessions.get(id);
|
|
353
|
+
const session = this.checkoutSessions.get(id);
|
|
312
354
|
if (!session)
|
|
313
355
|
return undefined;
|
|
314
356
|
session.status = 'canceled';
|
|
315
357
|
delete session.continue_url;
|
|
316
|
-
checkoutSessions.set(id, session);
|
|
358
|
+
this.checkoutSessions.set(id, session);
|
|
317
359
|
return session;
|
|
318
360
|
}
|
|
319
361
|
// Get order
|
|
320
362
|
getOrder(id) {
|
|
321
|
-
return orders.get(id);
|
|
363
|
+
return this.orders.get(id);
|
|
322
364
|
}
|
|
323
365
|
// List orders
|
|
324
366
|
listOrders() {
|
|
325
|
-
return Array.from(orders.values());
|
|
367
|
+
return Array.from(this.orders.values());
|
|
326
368
|
}
|
|
327
369
|
getConfig() {
|
|
328
370
|
return this.config;
|
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"version": "1.0.0",
|
|
2
|
+
"version": "1.1.0",
|
|
4
3
|
"description": "Generate UCP-compliant servers for merchants from a simple schema",
|
|
5
4
|
"main": "dist/index.js",
|
|
6
5
|
"types": "dist/index.d.ts",
|
|
@@ -24,8 +23,13 @@
|
|
|
24
23
|
"agentic",
|
|
25
24
|
"ai-commerce"
|
|
26
25
|
],
|
|
27
|
-
"author": "",
|
|
26
|
+
"author": "Hemanth HM <hemanth@h3manth.com>",
|
|
28
27
|
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/hemanth/ucpify.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/hemanth/ucpify#readme",
|
|
29
33
|
"type": "commonjs",
|
|
30
34
|
"dependencies": {
|
|
31
35
|
"@paypal/paypal-server-sdk": "^2.2.0",
|
|
@@ -49,5 +53,6 @@
|
|
|
49
53
|
"ts-node": "^10.9.2",
|
|
50
54
|
"typescript": "^5.9.3",
|
|
51
55
|
"vitest": "^4.0.18"
|
|
52
|
-
}
|
|
53
|
-
|
|
56
|
+
},
|
|
57
|
+
"name": "ucpify"
|
|
58
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -7,11 +7,12 @@ import { MerchantConfigSchema, MerchantConfig } from './schema';
|
|
|
7
7
|
import { createExpressApp } from './express-app';
|
|
8
8
|
|
|
9
9
|
const program = new Command();
|
|
10
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
10
11
|
|
|
11
12
|
program
|
|
12
|
-
.name('
|
|
13
|
+
.name('ucpify')
|
|
13
14
|
.description('Generate and run a UCP-compliant server for merchants')
|
|
14
|
-
.version(
|
|
15
|
+
.version(pkg.version);
|
|
15
16
|
|
|
16
17
|
program
|
|
17
18
|
.command('init')
|
package/src/express-app.ts
CHANGED
|
@@ -29,8 +29,8 @@ const checkoutLimiter = rateLimit({
|
|
|
29
29
|
|
|
30
30
|
export function createExpressApp(config: MerchantConfig, options?: { useDb?: boolean }): express.Application {
|
|
31
31
|
const app = express();
|
|
32
|
-
const ucpServer: UCPServerType = options?.useDb
|
|
33
|
-
? new UCPServerDB(config)
|
|
32
|
+
const ucpServer: UCPServerType = options?.useDb
|
|
33
|
+
? new UCPServerDB(config)
|
|
34
34
|
: new UCPServer(config);
|
|
35
35
|
const useDb = options?.useDb ?? false;
|
|
36
36
|
|
|
@@ -58,7 +58,7 @@ export function createExpressApp(config: MerchantConfig, options?: { useDb?: boo
|
|
|
58
58
|
const paymentIntent = event.data.object as { id: string; metadata?: { checkout_id?: string; order_id?: string } };
|
|
59
59
|
const checkoutId = paymentIntent.metadata?.checkout_id;
|
|
60
60
|
const orderId = paymentIntent.metadata?.order_id;
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
if (checkoutId && useDb) {
|
|
63
63
|
db.prepare(`UPDATE checkouts SET payment_status = 'succeeded', updated_at = ? WHERE id = ?`)
|
|
64
64
|
.run(new Date().toISOString(), checkoutId);
|
|
@@ -73,16 +73,16 @@ export function createExpressApp(config: MerchantConfig, options?: { useDb?: boo
|
|
|
73
73
|
const paymentIntent = event.data.object as { id: string; metadata?: { checkout_id?: string; order_id?: string }; last_payment_error?: { message?: string } };
|
|
74
74
|
const checkoutId = paymentIntent.metadata?.checkout_id;
|
|
75
75
|
const orderId = paymentIntent.metadata?.order_id;
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
if (checkoutId && useDb) {
|
|
78
78
|
db.prepare(`UPDATE checkouts SET payment_status = 'failed', updated_at = ? WHERE id = ?`)
|
|
79
79
|
.run(new Date().toISOString(), checkoutId);
|
|
80
80
|
if (orderId) {
|
|
81
81
|
db.prepare(`UPDATE orders SET payment_status = 'failed' WHERE id = ?`).run(orderId);
|
|
82
82
|
}
|
|
83
|
-
logger.error({
|
|
83
|
+
logger.error({
|
|
84
84
|
checkoutId, orderId, paymentIntentId: paymentIntent.id,
|
|
85
|
-
error: paymentIntent.last_payment_error?.message
|
|
85
|
+
error: paymentIntent.last_payment_error?.message
|
|
86
86
|
}, 'Payment failed');
|
|
87
87
|
}
|
|
88
88
|
break;
|
|
@@ -107,7 +107,7 @@ export function createExpressApp(config: MerchantConfig, options?: { useDb?: boo
|
|
|
107
107
|
// Get the checkout ID from the order
|
|
108
108
|
const orderDetails = await getPayPalOrder(resource.id);
|
|
109
109
|
const checkoutId = orderDetails?.customId;
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
if (checkoutId && useDb) {
|
|
112
112
|
const now = new Date().toISOString();
|
|
113
113
|
db.prepare(`UPDATE checkouts SET payment_status = 'succeeded', updated_at = ? WHERE paypal_order_id = ?`)
|
|
@@ -267,17 +267,17 @@ export function createExpressApp(config: MerchantConfig, options?: { useDb?: boo
|
|
|
267
267
|
const checkoutStats = db.prepare(`
|
|
268
268
|
SELECT status, COUNT(*) as count FROM checkouts GROUP BY status
|
|
269
269
|
`).all() as { status: string; count: number }[];
|
|
270
|
-
|
|
270
|
+
|
|
271
271
|
const orderStats = db.prepare(`
|
|
272
272
|
SELECT payment_status, payment_provider, COUNT(*) as count
|
|
273
273
|
FROM orders GROUP BY payment_status, payment_provider
|
|
274
274
|
`).all() as { payment_status: string; payment_provider: string; count: number }[];
|
|
275
|
-
|
|
275
|
+
|
|
276
276
|
const revenueResult = db.prepare(`
|
|
277
277
|
SELECT SUM(json_extract(totals_json, '$[?(@.type=="total")].amount')) as total
|
|
278
278
|
FROM orders WHERE payment_status = 'succeeded'
|
|
279
279
|
`).get() as { total: number | null };
|
|
280
|
-
|
|
280
|
+
|
|
281
281
|
const todayOrders = db.prepare(`
|
|
282
282
|
SELECT COUNT(*) as count FROM orders
|
|
283
283
|
WHERE date(created_at) = date('now')
|
|
@@ -302,6 +302,17 @@ export function createExpressApp(config: MerchantConfig, options?: { useDb?: boo
|
|
|
302
302
|
|
|
303
303
|
// Admin dashboard HTML
|
|
304
304
|
app.get('/admin', (_req: Request, res: Response) => {
|
|
305
|
+
const statsScript = useDb
|
|
306
|
+
? `fetch('/admin/stats').then(r=>r.json()).then(d=>{
|
|
307
|
+
document.getElementById('stats').innerHTML = \`
|
|
308
|
+
<div class="card"><div class="stat">\${Object.values(d.checkouts).reduce((a,b)=>a+b,0)}</div><div class="label">Total Checkouts</div></div>
|
|
309
|
+
<div class="card"><div class="stat">\${d.checkouts.completed||0}</div><div class="label">Completed</div></div>
|
|
310
|
+
<div class="card"><div class="stat">\${d.orders.today||0}</div><div class="label">Orders Today</div></div>
|
|
311
|
+
<div class="card"><div class="stat">$\${(d.revenue.total_succeeded/100).toFixed(2)}</div><div class="label">Revenue</div></div>
|
|
312
|
+
\`;
|
|
313
|
+
}).catch(e=>document.getElementById('stats').innerHTML='Error loading stats');`
|
|
314
|
+
: `document.getElementById('stats').innerHTML = '<div class="card" style="grid-column:1/-1"><div class="label">Running in <strong>in-memory mode</strong>. Stats require <code>--db</code> mode.</div><div style="margin-top:8px;font-size:0.85em;color:#999">Restart with: <code>ucpify serve config.json</code> (DB is on by default)</div></div>';`;
|
|
315
|
+
|
|
305
316
|
res.send(`<!DOCTYPE html>
|
|
306
317
|
<html><head><title>UCP Admin</title>
|
|
307
318
|
<style>
|
|
@@ -311,20 +322,12 @@ export function createExpressApp(config: MerchantConfig, options?: { useDb?: boo
|
|
|
311
322
|
.label { color: #666; font-size: 0.9em; }
|
|
312
323
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }
|
|
313
324
|
h1 { color: #333; }
|
|
325
|
+
code { background: #e5e5e5; padding: 2px 6px; border-radius: 3px; font-size: 0.85em; }
|
|
314
326
|
</style>
|
|
315
327
|
</head><body>
|
|
316
328
|
<h1>🛒 UCP Server Admin</h1>
|
|
317
329
|
<div class="grid" id="stats">Loading...</div>
|
|
318
|
-
<script>
|
|
319
|
-
fetch('/admin/stats').then(r=>r.json()).then(d=>{
|
|
320
|
-
document.getElementById('stats').innerHTML = \`
|
|
321
|
-
<div class="card"><div class="stat">\${Object.values(d.checkouts).reduce((a,b)=>a+b,0)}</div><div class="label">Total Checkouts</div></div>
|
|
322
|
-
<div class="card"><div class="stat">\${d.checkouts.completed||0}</div><div class="label">Completed</div></div>
|
|
323
|
-
<div class="card"><div class="stat">\${d.orders.today||0}</div><div class="label">Orders Today</div></div>
|
|
324
|
-
<div class="card"><div class="stat">$\${(d.revenue.total_succeeded/100).toFixed(2)}</div><div class="label">Revenue</div></div>
|
|
325
|
-
\`;
|
|
326
|
-
}).catch(e=>document.getElementById('stats').innerHTML='Error loading stats');
|
|
327
|
-
</script>
|
|
330
|
+
<script>${statsScript}</script>
|
|
328
331
|
</body></html>`);
|
|
329
332
|
});
|
|
330
333
|
|
|
@@ -332,8 +335,8 @@ fetch('/admin/stats').then(r=>r.json()).then(d=>{
|
|
|
332
335
|
app.get('/health', (_req: Request, res: Response) => {
|
|
333
336
|
const dbHealthy = isDbHealthy();
|
|
334
337
|
const status = dbHealthy ? 'ok' : 'degraded';
|
|
335
|
-
res.status(dbHealthy ? 200 : 503).json({
|
|
336
|
-
status,
|
|
338
|
+
res.status(dbHealthy ? 200 : 503).json({
|
|
339
|
+
status,
|
|
337
340
|
ucp_version: UCP_VERSION,
|
|
338
341
|
database: dbHealthy ? 'connected' : 'disconnected',
|
|
339
342
|
timestamp: new Date().toISOString(),
|
package/src/logger.ts
CHANGED
package/src/ucp-server.ts
CHANGED
|
@@ -11,14 +11,15 @@ import {
|
|
|
11
11
|
Item,
|
|
12
12
|
OrderConfirmation,
|
|
13
13
|
} from './schema';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const orders: Map<string, Order> = new Map();
|
|
14
|
+
import { createPaymentIntent, isStripeConfigured } from './stripe';
|
|
15
|
+
import { createPayPalOrder, isPayPalConfigured } from './paypal';
|
|
16
|
+
import { logger } from './logger';
|
|
18
17
|
|
|
19
18
|
export class UCPServer {
|
|
20
19
|
private config: MerchantConfig;
|
|
21
20
|
private itemsMap: Map<string, Item>;
|
|
21
|
+
private checkoutSessions: Map<string, CheckoutSession> = new Map();
|
|
22
|
+
private orders: Map<string, Order> = new Map();
|
|
22
23
|
|
|
23
24
|
constructor(config: MerchantConfig) {
|
|
24
25
|
this.config = config;
|
|
@@ -122,7 +123,7 @@ export class UCPServer {
|
|
|
122
123
|
// Create checkout session
|
|
123
124
|
createCheckout(data: { line_items: { id?: string; item: { id: string; title?: string; price?: number }; quantity: number }[] }): CheckoutSession {
|
|
124
125
|
const id = `chk_${uuidv4().replace(/-/g, '').slice(0, 12)}`;
|
|
125
|
-
|
|
126
|
+
|
|
126
127
|
// Resolve items from catalog or use provided
|
|
127
128
|
const lineItems: LineItem[] = data.line_items.map((li, idx) => {
|
|
128
129
|
const catalogItem = this.itemsMap.get(li.item.id);
|
|
@@ -131,7 +132,7 @@ export class UCPServer {
|
|
|
131
132
|
title: li.item.title || 'Unknown Item',
|
|
132
133
|
price: li.item.price || 0,
|
|
133
134
|
};
|
|
134
|
-
|
|
135
|
+
|
|
135
136
|
const subtotal = item.price * li.quantity;
|
|
136
137
|
return {
|
|
137
138
|
id: li.id || `li_${idx + 1}`,
|
|
@@ -183,13 +184,13 @@ export class UCPServer {
|
|
|
183
184
|
expires_at: expiresAt,
|
|
184
185
|
};
|
|
185
186
|
|
|
186
|
-
checkoutSessions.set(id, session);
|
|
187
|
+
this.checkoutSessions.set(id, session);
|
|
187
188
|
return session;
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
// Get checkout session
|
|
191
192
|
getCheckout(id: string): CheckoutSession | undefined {
|
|
192
|
-
return checkoutSessions.get(id);
|
|
193
|
+
return this.checkoutSessions.get(id);
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
// Update checkout session
|
|
@@ -198,7 +199,7 @@ export class UCPServer {
|
|
|
198
199
|
line_items?: { id?: string; item: { id: string; title?: string; price?: number }; quantity: number }[];
|
|
199
200
|
fulfillment?: Fulfillment;
|
|
200
201
|
}): CheckoutSession | undefined {
|
|
201
|
-
const session = checkoutSessions.get(id);
|
|
202
|
+
const session = this.checkoutSessions.get(id);
|
|
202
203
|
if (!session) return undefined;
|
|
203
204
|
|
|
204
205
|
// Update buyer
|
|
@@ -239,18 +240,18 @@ export class UCPServer {
|
|
|
239
240
|
const groups = method.groups?.length
|
|
240
241
|
? method.groups
|
|
241
242
|
: [
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
243
|
+
{
|
|
244
|
+
id: `group_${mIdx + 1}`,
|
|
245
|
+
line_item_ids: session.line_items.map((li) => li.id),
|
|
246
|
+
selected_option_id: this.config.shipping_options[0]?.id,
|
|
247
|
+
options: this.config.shipping_options.map((opt) => ({
|
|
248
|
+
id: opt.id,
|
|
249
|
+
title: opt.title,
|
|
250
|
+
description: opt.description || opt.estimated_days,
|
|
251
|
+
totals: [{ type: 'total', amount: opt.price }],
|
|
252
|
+
})),
|
|
253
|
+
},
|
|
254
|
+
];
|
|
254
255
|
|
|
255
256
|
return {
|
|
256
257
|
id: method.id || `method_${mIdx + 1}`,
|
|
@@ -293,20 +294,59 @@ export class UCPServer {
|
|
|
293
294
|
session.messages = newMessages.length > 0 ? newMessages : undefined;
|
|
294
295
|
session.status = newMessages.length > 0 ? 'incomplete' : 'ready_for_complete';
|
|
295
296
|
|
|
296
|
-
checkoutSessions.set(id, session);
|
|
297
|
+
this.checkoutSessions.set(id, session);
|
|
297
298
|
return session;
|
|
298
299
|
}
|
|
299
300
|
|
|
300
301
|
// Complete checkout (create order)
|
|
301
302
|
async completeCheckout(id: string): Promise<CheckoutSession | { error: string }> {
|
|
302
|
-
const session = checkoutSessions.get(id);
|
|
303
|
+
const session = this.checkoutSessions.get(id);
|
|
303
304
|
if (!session) return { error: 'Checkout session not found' };
|
|
304
305
|
if (session.status !== 'ready_for_complete') {
|
|
305
306
|
return { error: 'Checkout is not ready for completion', messages: session.messages };
|
|
306
307
|
}
|
|
307
308
|
|
|
308
309
|
const orderId = `order_${uuidv4().replace(/-/g, '').slice(0, 12)}`;
|
|
309
|
-
|
|
310
|
+
|
|
311
|
+
// Get total amount for payment
|
|
312
|
+
const totalAmount = session.totals.find(t => t.type === 'total')?.amount || 0;
|
|
313
|
+
|
|
314
|
+
// Payment processing - try Stripe first, then PayPal
|
|
315
|
+
if (totalAmount > 0) {
|
|
316
|
+
if (isStripeConfigured()) {
|
|
317
|
+
try {
|
|
318
|
+
const paymentIntent = await createPaymentIntent({
|
|
319
|
+
amount: totalAmount,
|
|
320
|
+
currency: session.currency,
|
|
321
|
+
checkoutId: id,
|
|
322
|
+
customerEmail: session.buyer?.email,
|
|
323
|
+
metadata: { order_id: orderId },
|
|
324
|
+
});
|
|
325
|
+
if (paymentIntent) {
|
|
326
|
+
logger.info({ checkoutId: id, paymentIntentId: paymentIntent.id, amount: totalAmount }, 'Stripe payment intent created');
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
logger.error({ error, checkoutId: id }, 'Failed to create Stripe payment intent');
|
|
330
|
+
return { error: 'Payment processing failed' };
|
|
331
|
+
}
|
|
332
|
+
} else if (isPayPalConfigured()) {
|
|
333
|
+
try {
|
|
334
|
+
const ppOrder = await createPayPalOrder({
|
|
335
|
+
amount: totalAmount,
|
|
336
|
+
currency: session.currency,
|
|
337
|
+
checkoutId: id,
|
|
338
|
+
description: `Order ${orderId}`,
|
|
339
|
+
});
|
|
340
|
+
if (ppOrder) {
|
|
341
|
+
logger.info({ checkoutId: id, paypalOrderId: ppOrder.id, amount: totalAmount }, 'PayPal order created');
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
logger.error({ error, checkoutId: id }, 'Failed to create PayPal order');
|
|
345
|
+
return { error: 'Payment processing failed' };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
310
350
|
const order: Order = {
|
|
311
351
|
ucp: {
|
|
312
352
|
version: UCP_VERSION,
|
|
@@ -322,25 +362,25 @@ export class UCPServer {
|
|
|
322
362
|
totals: session.totals,
|
|
323
363
|
fulfillment: session.fulfillment
|
|
324
364
|
? {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
365
|
+
expectations: session.fulfillment.methods.map((m, idx) => ({
|
|
366
|
+
id: `exp_${idx + 1}`,
|
|
367
|
+
line_items: m.line_item_ids.map((liId) => {
|
|
368
|
+
const li = session.line_items.find((l) => l.id === liId);
|
|
369
|
+
return { id: liId, quantity: li?.quantity || 1 };
|
|
370
|
+
}),
|
|
371
|
+
method_type: m.type,
|
|
372
|
+
destination: m.destinations.find((d) => d.id === m.selected_destination_id),
|
|
373
|
+
description: 'Arrives in 5-7 business days',
|
|
374
|
+
fulfillable_on: 'now',
|
|
375
|
+
})),
|
|
376
|
+
events: [],
|
|
377
|
+
}
|
|
338
378
|
: undefined,
|
|
339
379
|
created_at: new Date().toISOString(),
|
|
340
380
|
};
|
|
341
381
|
|
|
342
|
-
orders.set(orderId, order);
|
|
343
|
-
|
|
382
|
+
this.orders.set(orderId, order);
|
|
383
|
+
|
|
344
384
|
// Update session to completed state with order confirmation
|
|
345
385
|
session.status = 'completed';
|
|
346
386
|
session.order = {
|
|
@@ -349,29 +389,29 @@ export class UCPServer {
|
|
|
349
389
|
};
|
|
350
390
|
delete session.continue_url;
|
|
351
391
|
delete session.messages;
|
|
352
|
-
checkoutSessions.set(id, session);
|
|
392
|
+
this.checkoutSessions.set(id, session);
|
|
353
393
|
|
|
354
394
|
return session;
|
|
355
395
|
}
|
|
356
396
|
|
|
357
397
|
// Cancel checkout
|
|
358
398
|
cancelCheckout(id: string): CheckoutSession | undefined {
|
|
359
|
-
const session = checkoutSessions.get(id);
|
|
399
|
+
const session = this.checkoutSessions.get(id);
|
|
360
400
|
if (!session) return undefined;
|
|
361
401
|
session.status = 'canceled';
|
|
362
402
|
delete session.continue_url;
|
|
363
|
-
checkoutSessions.set(id, session);
|
|
403
|
+
this.checkoutSessions.set(id, session);
|
|
364
404
|
return session;
|
|
365
405
|
}
|
|
366
406
|
|
|
367
407
|
// Get order
|
|
368
408
|
getOrder(id: string): Order | undefined {
|
|
369
|
-
return orders.get(id);
|
|
409
|
+
return this.orders.get(id);
|
|
370
410
|
}
|
|
371
411
|
|
|
372
412
|
// List orders
|
|
373
413
|
listOrders(): Order[] {
|
|
374
|
-
return Array.from(orders.values());
|
|
414
|
+
return Array.from(this.orders.values());
|
|
375
415
|
}
|
|
376
416
|
|
|
377
417
|
getConfig(): MerchantConfig {
|