ucpify 1.0.1 → 1.2.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.
@@ -57,6 +57,11 @@ export class UCPServerDB {
57
57
  'dev.ucp.shopping.order': [
58
58
  { version: UCP_VERSION, spec: 'https://ucp.dev/specification/order', schema: 'https://ucp.dev/schemas/shopping/order.json' },
59
59
  ],
60
+ ...(this.config.oauth ? {
61
+ 'dev.ucp.identity_linking': [
62
+ { version: UCP_VERSION, spec: 'https://ucp.dev/specification/identity-linking' },
63
+ ],
64
+ } : {}),
60
65
  },
61
66
  payment_handlers: paymentHandlers,
62
67
  },
@@ -105,9 +110,9 @@ export class UCPServerDB {
105
110
 
106
111
  private sessionFromRow(row: any): CheckoutSession | undefined {
107
112
  if (!row) return undefined;
108
-
113
+
109
114
  const lineItems = db.prepare('SELECT * FROM line_items WHERE checkout_id = ?').all(row.id) as any[];
110
-
115
+
111
116
  return {
112
117
  ucp: {
113
118
  version: UCP_VERSION,
@@ -169,7 +174,7 @@ export class UCPServerDB {
169
174
  `);
170
175
 
171
176
  db.transaction(() => {
172
- insertCheckout.run(id, status, this.config.currency, JSON.stringify(totals), JSON.stringify(links),
177
+ insertCheckout.run(id, status, this.config.currency, JSON.stringify(totals), JSON.stringify(links),
173
178
  messages.length > 0 ? JSON.stringify(messages) : null, continueUrl, expiresAt, now, now);
174
179
  for (const li of lineItems) {
175
180
  insertLineItem.run(li.id, id, JSON.stringify(li.item), li.quantity, JSON.stringify(li.totals));
@@ -284,13 +289,13 @@ export class UCPServerDB {
284
289
 
285
290
  // Get total amount for payment
286
291
  const totalAmount = session.totals.find(t => t.type === 'total')?.amount || 0;
287
-
292
+
288
293
  // Payment processing - try Stripe first, then PayPal
289
294
  let paymentIntentId: string | null = null;
290
295
  let paypalOrderId: string | null = null;
291
296
  let paymentProvider: string | null = null;
292
297
  let paymentStatus = 'none';
293
-
298
+
294
299
  if (totalAmount > 0) {
295
300
  // Try Stripe first
296
301
  if (isStripeConfigured()) {
package/src/ucp-server.ts CHANGED
@@ -11,14 +11,15 @@ import {
11
11
  Item,
12
12
  OrderConfirmation,
13
13
  } from './schema';
14
-
15
- // In-memory stores
16
- const checkoutSessions: Map<string, CheckoutSession> = new Map();
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;
@@ -75,6 +76,14 @@ export class UCPServer {
75
76
  schema: 'https://ucp.dev/schemas/shopping/order.json',
76
77
  },
77
78
  ],
79
+ ...(this.config.oauth ? {
80
+ 'dev.ucp.identity_linking': [
81
+ {
82
+ version: UCP_VERSION,
83
+ spec: 'https://ucp.dev/specification/identity-linking',
84
+ },
85
+ ],
86
+ } : {}),
78
87
  },
79
88
  payment_handlers: paymentHandlers,
80
89
  },
@@ -122,7 +131,7 @@ export class UCPServer {
122
131
  // Create checkout session
123
132
  createCheckout(data: { line_items: { id?: string; item: { id: string; title?: string; price?: number }; quantity: number }[] }): CheckoutSession {
124
133
  const id = `chk_${uuidv4().replace(/-/g, '').slice(0, 12)}`;
125
-
134
+
126
135
  // Resolve items from catalog or use provided
127
136
  const lineItems: LineItem[] = data.line_items.map((li, idx) => {
128
137
  const catalogItem = this.itemsMap.get(li.item.id);
@@ -131,7 +140,7 @@ export class UCPServer {
131
140
  title: li.item.title || 'Unknown Item',
132
141
  price: li.item.price || 0,
133
142
  };
134
-
143
+
135
144
  const subtotal = item.price * li.quantity;
136
145
  return {
137
146
  id: li.id || `li_${idx + 1}`,
@@ -183,13 +192,13 @@ export class UCPServer {
183
192
  expires_at: expiresAt,
184
193
  };
185
194
 
186
- checkoutSessions.set(id, session);
195
+ this.checkoutSessions.set(id, session);
187
196
  return session;
188
197
  }
189
198
 
190
199
  // Get checkout session
191
200
  getCheckout(id: string): CheckoutSession | undefined {
192
- return checkoutSessions.get(id);
201
+ return this.checkoutSessions.get(id);
193
202
  }
194
203
 
195
204
  // Update checkout session
@@ -198,7 +207,7 @@ export class UCPServer {
198
207
  line_items?: { id?: string; item: { id: string; title?: string; price?: number }; quantity: number }[];
199
208
  fulfillment?: Fulfillment;
200
209
  }): CheckoutSession | undefined {
201
- const session = checkoutSessions.get(id);
210
+ const session = this.checkoutSessions.get(id);
202
211
  if (!session) return undefined;
203
212
 
204
213
  // Update buyer
@@ -239,18 +248,18 @@ export class UCPServer {
239
248
  const groups = method.groups?.length
240
249
  ? method.groups
241
250
  : [
242
- {
243
- id: `group_${mIdx + 1}`,
244
- line_item_ids: session.line_items.map((li) => li.id),
245
- selected_option_id: this.config.shipping_options[0]?.id,
246
- options: this.config.shipping_options.map((opt) => ({
247
- id: opt.id,
248
- title: opt.title,
249
- description: opt.description || opt.estimated_days,
250
- totals: [{ type: 'total', amount: opt.price }],
251
- })),
252
- },
253
- ];
251
+ {
252
+ id: `group_${mIdx + 1}`,
253
+ line_item_ids: session.line_items.map((li) => li.id),
254
+ selected_option_id: this.config.shipping_options[0]?.id,
255
+ options: this.config.shipping_options.map((opt) => ({
256
+ id: opt.id,
257
+ title: opt.title,
258
+ description: opt.description || opt.estimated_days,
259
+ totals: [{ type: 'total', amount: opt.price }],
260
+ })),
261
+ },
262
+ ];
254
263
 
255
264
  return {
256
265
  id: method.id || `method_${mIdx + 1}`,
@@ -293,20 +302,59 @@ export class UCPServer {
293
302
  session.messages = newMessages.length > 0 ? newMessages : undefined;
294
303
  session.status = newMessages.length > 0 ? 'incomplete' : 'ready_for_complete';
295
304
 
296
- checkoutSessions.set(id, session);
305
+ this.checkoutSessions.set(id, session);
297
306
  return session;
298
307
  }
299
308
 
300
309
  // Complete checkout (create order)
301
310
  async completeCheckout(id: string): Promise<CheckoutSession | { error: string }> {
302
- const session = checkoutSessions.get(id);
311
+ const session = this.checkoutSessions.get(id);
303
312
  if (!session) return { error: 'Checkout session not found' };
304
313
  if (session.status !== 'ready_for_complete') {
305
314
  return { error: 'Checkout is not ready for completion', messages: session.messages };
306
315
  }
307
316
 
308
317
  const orderId = `order_${uuidv4().replace(/-/g, '').slice(0, 12)}`;
309
-
318
+
319
+ // Get total amount for payment
320
+ const totalAmount = session.totals.find(t => t.type === 'total')?.amount || 0;
321
+
322
+ // Payment processing - try Stripe first, then PayPal
323
+ if (totalAmount > 0) {
324
+ if (isStripeConfigured()) {
325
+ try {
326
+ const paymentIntent = await createPaymentIntent({
327
+ amount: totalAmount,
328
+ currency: session.currency,
329
+ checkoutId: id,
330
+ customerEmail: session.buyer?.email,
331
+ metadata: { order_id: orderId },
332
+ });
333
+ if (paymentIntent) {
334
+ logger.info({ checkoutId: id, paymentIntentId: paymentIntent.id, amount: totalAmount }, 'Stripe payment intent created');
335
+ }
336
+ } catch (error) {
337
+ logger.error({ error, checkoutId: id }, 'Failed to create Stripe payment intent');
338
+ return { error: 'Payment processing failed' };
339
+ }
340
+ } else if (isPayPalConfigured()) {
341
+ try {
342
+ const ppOrder = await createPayPalOrder({
343
+ amount: totalAmount,
344
+ currency: session.currency,
345
+ checkoutId: id,
346
+ description: `Order ${orderId}`,
347
+ });
348
+ if (ppOrder) {
349
+ logger.info({ checkoutId: id, paypalOrderId: ppOrder.id, amount: totalAmount }, 'PayPal order created');
350
+ }
351
+ } catch (error) {
352
+ logger.error({ error, checkoutId: id }, 'Failed to create PayPal order');
353
+ return { error: 'Payment processing failed' };
354
+ }
355
+ }
356
+ }
357
+
310
358
  const order: Order = {
311
359
  ucp: {
312
360
  version: UCP_VERSION,
@@ -322,25 +370,25 @@ export class UCPServer {
322
370
  totals: session.totals,
323
371
  fulfillment: session.fulfillment
324
372
  ? {
325
- expectations: session.fulfillment.methods.map((m, idx) => ({
326
- id: `exp_${idx + 1}`,
327
- line_items: m.line_item_ids.map((liId) => {
328
- const li = session.line_items.find((l) => l.id === liId);
329
- return { id: liId, quantity: li?.quantity || 1 };
330
- }),
331
- method_type: m.type,
332
- destination: m.destinations.find((d) => d.id === m.selected_destination_id),
333
- description: 'Arrives in 5-7 business days',
334
- fulfillable_on: 'now',
335
- })),
336
- events: [],
337
- }
373
+ expectations: session.fulfillment.methods.map((m, idx) => ({
374
+ id: `exp_${idx + 1}`,
375
+ line_items: m.line_item_ids.map((liId) => {
376
+ const li = session.line_items.find((l) => l.id === liId);
377
+ return { id: liId, quantity: li?.quantity || 1 };
378
+ }),
379
+ method_type: m.type,
380
+ destination: m.destinations.find((d) => d.id === m.selected_destination_id),
381
+ description: 'Arrives in 5-7 business days',
382
+ fulfillable_on: 'now',
383
+ })),
384
+ events: [],
385
+ }
338
386
  : undefined,
339
387
  created_at: new Date().toISOString(),
340
388
  };
341
389
 
342
- orders.set(orderId, order);
343
-
390
+ this.orders.set(orderId, order);
391
+
344
392
  // Update session to completed state with order confirmation
345
393
  session.status = 'completed';
346
394
  session.order = {
@@ -349,29 +397,29 @@ export class UCPServer {
349
397
  };
350
398
  delete session.continue_url;
351
399
  delete session.messages;
352
- checkoutSessions.set(id, session);
400
+ this.checkoutSessions.set(id, session);
353
401
 
354
402
  return session;
355
403
  }
356
404
 
357
405
  // Cancel checkout
358
406
  cancelCheckout(id: string): CheckoutSession | undefined {
359
- const session = checkoutSessions.get(id);
407
+ const session = this.checkoutSessions.get(id);
360
408
  if (!session) return undefined;
361
409
  session.status = 'canceled';
362
410
  delete session.continue_url;
363
- checkoutSessions.set(id, session);
411
+ this.checkoutSessions.set(id, session);
364
412
  return session;
365
413
  }
366
414
 
367
415
  // Get order
368
416
  getOrder(id: string): Order | undefined {
369
- return orders.get(id);
417
+ return this.orders.get(id);
370
418
  }
371
419
 
372
420
  // List orders
373
421
  listOrders(): Order[] {
374
- return Array.from(orders.values());
422
+ return Array.from(this.orders.values());
375
423
  }
376
424
 
377
425
  getConfig(): MerchantConfig {
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ exclude: ['dist/**', 'node_modules/**'],
6
+ },
7
+ });