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