ucpify 1.1.0 → 1.2.1

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.
@@ -1,373 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UCPServer = void 0;
4
- const uuid_1 = require("uuid");
5
- const schema_1 = require("./schema");
6
- const stripe_1 = require("./stripe");
7
- const paypal_1 = require("./paypal");
8
- const logger_1 = require("./logger");
9
- class UCPServer {
10
- config;
11
- itemsMap;
12
- checkoutSessions = new Map();
13
- orders = new Map();
14
- constructor(config) {
15
- this.config = config;
16
- this.itemsMap = new Map(config.items.map((item) => [item.id, item]));
17
- }
18
- // Generate UCP profile for /.well-known/ucp
19
- getProfile() {
20
- const paymentHandlers = {};
21
- for (const handler of this.config.payment_handlers) {
22
- paymentHandlers[handler.namespace] = [
23
- {
24
- id: handler.id,
25
- version: schema_1.UCP_VERSION,
26
- config: handler.config,
27
- },
28
- ];
29
- }
30
- return {
31
- ucp: {
32
- version: schema_1.UCP_VERSION,
33
- services: {
34
- 'dev.ucp.shopping': [
35
- {
36
- version: schema_1.UCP_VERSION,
37
- spec: 'https://ucp.dev/specification/overview',
38
- transport: 'rest',
39
- endpoint: `${this.config.domain}/ucp/v1`,
40
- schema: 'https://ucp.dev/services/shopping/rest.openapi.json',
41
- },
42
- ],
43
- },
44
- capabilities: {
45
- 'dev.ucp.shopping.checkout': [
46
- {
47
- version: schema_1.UCP_VERSION,
48
- spec: 'https://ucp.dev/specification/checkout',
49
- schema: 'https://ucp.dev/schemas/shopping/checkout.json',
50
- },
51
- ],
52
- 'dev.ucp.shopping.fulfillment': [
53
- {
54
- version: schema_1.UCP_VERSION,
55
- spec: 'https://ucp.dev/specification/fulfillment',
56
- schema: 'https://ucp.dev/schemas/shopping/fulfillment.json',
57
- extends: 'dev.ucp.shopping.checkout',
58
- },
59
- ],
60
- 'dev.ucp.shopping.order': [
61
- {
62
- version: schema_1.UCP_VERSION,
63
- spec: 'https://ucp.dev/specification/order',
64
- schema: 'https://ucp.dev/schemas/shopping/order.json',
65
- },
66
- ],
67
- },
68
- payment_handlers: paymentHandlers,
69
- },
70
- };
71
- }
72
- // Calculate totals for line items
73
- calculateLineItemTotals(lineItems) {
74
- const subtotal = lineItems.reduce((sum, li) => sum + li.item.price * li.quantity, 0);
75
- const tax = Math.round(subtotal * this.config.tax_rate);
76
- return [
77
- { type: 'subtotal', amount: subtotal },
78
- { type: 'tax', amount: tax },
79
- { type: 'total', amount: subtotal + tax },
80
- ];
81
- }
82
- // Validate checkout and generate messages
83
- validateCheckout(session) {
84
- const messages = [];
85
- if (!session.buyer?.email) {
86
- messages.push({
87
- type: 'error',
88
- code: 'missing',
89
- path: '$.buyer.email',
90
- content: 'Buyer email is required',
91
- severity: 'recoverable',
92
- });
93
- }
94
- if (!session.fulfillment?.methods?.[0]?.selected_destination_id) {
95
- messages.push({
96
- type: 'error',
97
- code: 'missing',
98
- path: '$.fulfillment.methods[0].selected_destination_id',
99
- content: 'Shipping address is required',
100
- severity: 'recoverable',
101
- });
102
- }
103
- return messages;
104
- }
105
- // Create checkout session
106
- createCheckout(data) {
107
- const id = `chk_${(0, uuid_1.v4)().replace(/-/g, '').slice(0, 12)}`;
108
- // Resolve items from catalog or use provided
109
- const lineItems = data.line_items.map((li, idx) => {
110
- const catalogItem = this.itemsMap.get(li.item.id);
111
- const item = catalogItem || {
112
- id: li.item.id,
113
- title: li.item.title || 'Unknown Item',
114
- price: li.item.price || 0,
115
- };
116
- const subtotal = item.price * li.quantity;
117
- return {
118
- id: li.id || `li_${idx + 1}`,
119
- item,
120
- quantity: li.quantity,
121
- totals: [
122
- { type: 'subtotal', amount: subtotal },
123
- { type: 'total', amount: subtotal },
124
- ],
125
- };
126
- });
127
- const totals = this.calculateLineItemTotals(lineItems);
128
- const messages = this.validateCheckout({ line_items: lineItems });
129
- const paymentHandlers = {};
130
- for (const handler of this.config.payment_handlers) {
131
- paymentHandlers[handler.namespace] = [{ id: handler.id, version: schema_1.UCP_VERSION, config: handler.config }];
132
- }
133
- // Build links
134
- const links = [];
135
- if (this.config.terms_url) {
136
- links.push({ type: 'terms_of_service', url: this.config.terms_url });
137
- }
138
- if (this.config.privacy_url) {
139
- links.push({ type: 'privacy_policy', url: this.config.privacy_url });
140
- }
141
- // Set expiry (1 hour from now)
142
- const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString();
143
- const session = {
144
- ucp: {
145
- version: schema_1.UCP_VERSION,
146
- capabilities: {
147
- 'dev.ucp.shopping.checkout': [{ version: schema_1.UCP_VERSION }],
148
- },
149
- payment_handlers: paymentHandlers,
150
- },
151
- id,
152
- status: messages.length > 0 ? 'incomplete' : 'ready_for_complete',
153
- messages: messages.length > 0 ? messages : undefined,
154
- currency: this.config.currency,
155
- line_items: lineItems,
156
- totals,
157
- links,
158
- continue_url: `${this.config.domain}/checkout/${id}`,
159
- expires_at: expiresAt,
160
- };
161
- this.checkoutSessions.set(id, session);
162
- return session;
163
- }
164
- // Get checkout session
165
- getCheckout(id) {
166
- return this.checkoutSessions.get(id);
167
- }
168
- // Update checkout session
169
- updateCheckout(id, data) {
170
- const session = this.checkoutSessions.get(id);
171
- if (!session)
172
- return undefined;
173
- // Update buyer
174
- if (data.buyer) {
175
- session.buyer = { ...session.buyer, ...data.buyer };
176
- }
177
- // Update line items
178
- if (data.line_items) {
179
- session.line_items = data.line_items.map((li, idx) => {
180
- const catalogItem = this.itemsMap.get(li.item.id);
181
- const item = catalogItem || {
182
- id: li.item.id,
183
- title: li.item.title || 'Unknown Item',
184
- price: li.item.price || 0,
185
- };
186
- const subtotal = item.price * li.quantity;
187
- return {
188
- id: li.id || `li_${idx + 1}`,
189
- item,
190
- quantity: li.quantity,
191
- totals: [
192
- { type: 'subtotal', amount: subtotal },
193
- { type: 'total', amount: subtotal },
194
- ],
195
- };
196
- });
197
- }
198
- // Update fulfillment with shipping options
199
- if (data.fulfillment) {
200
- const methods = data.fulfillment.methods.map((method, mIdx) => {
201
- const destinations = method.destinations.map((dest, dIdx) => ({
202
- ...dest,
203
- id: dest.id || `dest_${dIdx + 1}`,
204
- }));
205
- const groups = method.groups?.length
206
- ? method.groups
207
- : [
208
- {
209
- id: `group_${mIdx + 1}`,
210
- line_item_ids: session.line_items.map((li) => li.id),
211
- selected_option_id: this.config.shipping_options[0]?.id,
212
- options: this.config.shipping_options.map((opt) => ({
213
- id: opt.id,
214
- title: opt.title,
215
- description: opt.description || opt.estimated_days,
216
- totals: [{ type: 'total', amount: opt.price }],
217
- })),
218
- },
219
- ];
220
- return {
221
- id: method.id || `method_${mIdx + 1}`,
222
- type: method.type,
223
- line_item_ids: method.line_item_ids || session.line_items.map((li) => li.id),
224
- selected_destination_id: method.selected_destination_id || destinations[0]?.id,
225
- destinations,
226
- groups,
227
- };
228
- });
229
- session.fulfillment = { methods };
230
- }
231
- // Recalculate totals including shipping
232
- let shippingTotal = 0;
233
- if (session.fulfillment) {
234
- for (const method of session.fulfillment.methods) {
235
- for (const group of method.groups) {
236
- const selectedOption = group.options.find((o) => o.id === group.selected_option_id);
237
- if (selectedOption) {
238
- const total = selectedOption.totals.find((t) => t.type === 'total');
239
- shippingTotal += total?.amount || 0;
240
- }
241
- }
242
- }
243
- }
244
- const subtotal = session.line_items.reduce((sum, li) => sum + li.item.price * li.quantity, 0);
245
- const tax = Math.round(subtotal * this.config.tax_rate);
246
- session.totals = [
247
- { type: 'subtotal', amount: subtotal },
248
- { type: 'shipping', amount: shippingTotal },
249
- { type: 'tax', amount: tax },
250
- { type: 'total', amount: subtotal + shippingTotal + tax },
251
- ];
252
- // Revalidate
253
- const newMessages = this.validateCheckout(session);
254
- session.messages = newMessages.length > 0 ? newMessages : undefined;
255
- session.status = newMessages.length > 0 ? 'incomplete' : 'ready_for_complete';
256
- this.checkoutSessions.set(id, session);
257
- return session;
258
- }
259
- // Complete checkout (create order)
260
- async completeCheckout(id) {
261
- const session = this.checkoutSessions.get(id);
262
- if (!session)
263
- return { error: 'Checkout session not found' };
264
- if (session.status !== 'ready_for_complete') {
265
- return { error: 'Checkout is not ready for completion', messages: session.messages };
266
- }
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
- }
308
- const order = {
309
- ucp: {
310
- version: schema_1.UCP_VERSION,
311
- capabilities: {
312
- 'dev.ucp.shopping.order': [{ version: schema_1.UCP_VERSION }],
313
- },
314
- },
315
- id: orderId,
316
- checkout_id: id,
317
- permalink_url: `${this.config.domain}/orders/${orderId}`,
318
- line_items: session.line_items,
319
- buyer: session.buyer,
320
- totals: session.totals,
321
- fulfillment: session.fulfillment
322
- ? {
323
- expectations: session.fulfillment.methods.map((m, idx) => ({
324
- id: `exp_${idx + 1}`,
325
- line_items: m.line_item_ids.map((liId) => {
326
- const li = session.line_items.find((l) => l.id === liId);
327
- return { id: liId, quantity: li?.quantity || 1 };
328
- }),
329
- method_type: m.type,
330
- destination: m.destinations.find((d) => d.id === m.selected_destination_id),
331
- description: 'Arrives in 5-7 business days',
332
- fulfillable_on: 'now',
333
- })),
334
- events: [],
335
- }
336
- : undefined,
337
- created_at: new Date().toISOString(),
338
- };
339
- this.orders.set(orderId, order);
340
- // Update session to completed state with order confirmation
341
- session.status = 'completed';
342
- session.order = {
343
- id: orderId,
344
- permalink_url: order.permalink_url,
345
- };
346
- delete session.continue_url;
347
- delete session.messages;
348
- this.checkoutSessions.set(id, session);
349
- return session;
350
- }
351
- // Cancel checkout
352
- cancelCheckout(id) {
353
- const session = this.checkoutSessions.get(id);
354
- if (!session)
355
- return undefined;
356
- session.status = 'canceled';
357
- delete session.continue_url;
358
- this.checkoutSessions.set(id, session);
359
- return session;
360
- }
361
- // Get order
362
- getOrder(id) {
363
- return this.orders.get(id);
364
- }
365
- // List orders
366
- listOrders() {
367
- return Array.from(this.orders.values());
368
- }
369
- getConfig() {
370
- return this.config;
371
- }
372
- }
373
- exports.UCPServer = UCPServer;