ucpify 1.0.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/src/paypal.ts ADDED
@@ -0,0 +1,120 @@
1
+ import { Client, Environment, OrdersController, ApiError, CheckoutPaymentIntent } from '@paypal/paypal-server-sdk';
2
+ import { config } from './config';
3
+ import { logger } from './logger';
4
+
5
+ let paypalClient: Client | null = null;
6
+ let ordersController: OrdersController | null = null;
7
+
8
+ function getPayPalClient(): Client | null {
9
+ if (!config.paypalClientId || !config.paypalClientSecret) {
10
+ return null;
11
+ }
12
+ if (!paypalClient) {
13
+ paypalClient = new Client({
14
+ clientCredentialsAuthCredentials: {
15
+ oAuthClientId: config.paypalClientId,
16
+ oAuthClientSecret: config.paypalClientSecret,
17
+ },
18
+ environment: config.paypalMode === 'live' ? Environment.Production : Environment.Sandbox,
19
+ });
20
+ ordersController = new OrdersController(paypalClient);
21
+ logger.info('PayPal client initialized');
22
+ }
23
+ return paypalClient;
24
+ }
25
+
26
+ export function isPayPalConfigured(): boolean {
27
+ return !!(config.paypalClientId && config.paypalClientSecret);
28
+ }
29
+
30
+ export interface CreatePayPalOrderParams {
31
+ amount: number; // in cents
32
+ currency: string;
33
+ checkoutId: string;
34
+ description?: string;
35
+ }
36
+
37
+ export async function createPayPalOrder(params: CreatePayPalOrderParams): Promise<{ id: string; status: string } | null> {
38
+ const client = getPayPalClient();
39
+ if (!client || !ordersController) {
40
+ logger.warn('PayPal not configured, skipping order creation');
41
+ return null;
42
+ }
43
+
44
+ // Convert cents to dollars with 2 decimal places
45
+ const amountValue = (params.amount / 100).toFixed(2);
46
+
47
+ try {
48
+ const response = await ordersController.createOrder({
49
+ body: {
50
+ intent: CheckoutPaymentIntent.Capture,
51
+ purchaseUnits: [
52
+ {
53
+ amount: {
54
+ currencyCode: params.currency.toUpperCase(),
55
+ value: amountValue,
56
+ },
57
+ description: params.description || `Checkout ${params.checkoutId}`,
58
+ customId: params.checkoutId,
59
+ },
60
+ ],
61
+ },
62
+ prefer: 'return=representation',
63
+ });
64
+
65
+ const order = response.result;
66
+ logger.info({
67
+ paypalOrderId: order.id,
68
+ checkoutId: params.checkoutId,
69
+ amount: params.amount,
70
+ }, 'PayPal order created');
71
+
72
+ return { id: order.id!, status: order.status! };
73
+ } catch (error) {
74
+ if (error instanceof ApiError) {
75
+ logger.error({ error: error.message, checkoutId: params.checkoutId }, 'Failed to create PayPal order');
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ export async function capturePayPalOrder(orderId: string): Promise<{ id: string; status: string } | null> {
82
+ const client = getPayPalClient();
83
+ if (!client || !ordersController) return null;
84
+
85
+ try {
86
+ const response = await ordersController.captureOrder({
87
+ id: orderId,
88
+ prefer: 'return=representation',
89
+ });
90
+
91
+ const order = response.result;
92
+ logger.info({ paypalOrderId: orderId, status: order.status }, 'PayPal order captured');
93
+ return { id: order.id!, status: order.status! };
94
+ } catch (error) {
95
+ if (error instanceof ApiError) {
96
+ logger.error({ error: error.message, paypalOrderId: orderId }, 'Failed to capture PayPal order');
97
+ }
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ export async function getPayPalOrder(orderId: string): Promise<{ id: string; status: string; customId?: string } | null> {
103
+ const client = getPayPalClient();
104
+ if (!client || !ordersController) return null;
105
+
106
+ try {
107
+ const response = await ordersController.getOrder({ id: orderId });
108
+ const order = response.result;
109
+ return {
110
+ id: order.id!,
111
+ status: order.status!,
112
+ customId: order.purchaseUnits?.[0]?.customId,
113
+ };
114
+ } catch (error) {
115
+ if (error instanceof ApiError) {
116
+ logger.error({ error: error.message, paypalOrderId: orderId }, 'Failed to get PayPal order');
117
+ }
118
+ throw error;
119
+ }
120
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,244 @@
1
+ import { z } from 'zod';
2
+
3
+ // UCP Version
4
+ export const UCP_VERSION = '2026-01-11';
5
+
6
+ // Product/Item Schema
7
+ export const ItemSchema = z.object({
8
+ id: z.string(),
9
+ title: z.string(),
10
+ description: z.string().optional(),
11
+ price: z.number().int().min(0), // Minor units (cents)
12
+ image_url: z.string().url().optional(),
13
+ sku: z.string().optional(),
14
+ stock: z.number().int().min(0).optional(), // Inventory tracking
15
+ });
16
+
17
+ // Shipping Option Schema
18
+ export const ShippingOptionSchema = z.object({
19
+ id: z.string(),
20
+ title: z.string(),
21
+ description: z.string().optional(),
22
+ price: z.number().int().min(0), // Minor units
23
+ estimated_days: z.string().optional(),
24
+ });
25
+
26
+ // Payment Handler Schema
27
+ export const PaymentHandlerSchema = z.object({
28
+ namespace: z.string(), // e.g., "com.stripe", "com.google.pay"
29
+ id: z.string(),
30
+ config: z.record(z.string(), z.unknown()).optional(),
31
+ });
32
+
33
+ // Merchant Configuration Schema
34
+ export const MerchantConfigSchema = z.object({
35
+ name: z.string(),
36
+ domain: z.string().url(),
37
+ currency: z.string().default('USD'),
38
+ terms_url: z.string().url().optional(),
39
+ privacy_url: z.string().url().optional(),
40
+ items: z.array(ItemSchema),
41
+ shipping_options: z.array(ShippingOptionSchema).default([]),
42
+ payment_handlers: z.array(PaymentHandlerSchema).default([]),
43
+ tax_rate: z.number().min(0).max(1).default(0), // e.g., 0.08 for 8%
44
+ port: z.number().default(3000),
45
+ });
46
+
47
+ export type Item = z.infer<typeof ItemSchema>;
48
+ export type ShippingOption = z.infer<typeof ShippingOptionSchema>;
49
+ export type PaymentHandler = z.infer<typeof PaymentHandlerSchema>;
50
+ export type MerchantConfig = z.infer<typeof MerchantConfigSchema>;
51
+
52
+ // Checkout Session Types
53
+ export type CheckoutStatus = 'incomplete' | 'requires_escalation' | 'ready_for_complete' | 'complete_in_progress' | 'completed' | 'canceled';
54
+
55
+ export interface Buyer {
56
+ email?: string;
57
+ first_name?: string;
58
+ last_name?: string;
59
+ phone?: string;
60
+ }
61
+
62
+ export interface Address {
63
+ id?: string;
64
+ first_name?: string;
65
+ last_name?: string;
66
+ street_address: string;
67
+ address_locality: string;
68
+ address_region: string;
69
+ postal_code: string;
70
+ address_country: string;
71
+ }
72
+
73
+ export interface LineItem {
74
+ id: string;
75
+ item: Item;
76
+ quantity: number;
77
+ totals?: { type: string; amount: number }[];
78
+ }
79
+
80
+ export interface FulfillmentOption {
81
+ id: string;
82
+ title: string;
83
+ description?: string;
84
+ totals: { type: string; amount: number }[];
85
+ }
86
+
87
+ export interface FulfillmentGroup {
88
+ id: string;
89
+ line_item_ids: string[];
90
+ selected_option_id?: string;
91
+ options: FulfillmentOption[];
92
+ }
93
+
94
+ export interface FulfillmentMethod {
95
+ id: string;
96
+ type: 'shipping' | 'pickup' | 'digital';
97
+ line_item_ids: string[];
98
+ selected_destination_id?: string;
99
+ destinations: Address[];
100
+ groups: FulfillmentGroup[];
101
+ }
102
+
103
+ export interface Fulfillment {
104
+ methods: FulfillmentMethod[];
105
+ }
106
+
107
+ export interface PaymentInstrument {
108
+ id: string;
109
+ handler_id: string;
110
+ type: string;
111
+ selected?: boolean;
112
+ display?: Record<string, unknown>;
113
+ }
114
+
115
+ export interface Payment {
116
+ instruments: PaymentInstrument[];
117
+ }
118
+
119
+ export interface CheckoutMessage {
120
+ type: 'error' | 'warning' | 'info';
121
+ code: string;
122
+ path?: string;
123
+ content: string;
124
+ severity: 'recoverable' | 'requires_buyer_input' | 'requires_buyer_review';
125
+ }
126
+
127
+ export interface OrderConfirmation {
128
+ id: string;
129
+ permalink_url?: string;
130
+ }
131
+
132
+ export interface CheckoutSession {
133
+ ucp: {
134
+ version: string;
135
+ capabilities: Record<string, { version: string }[]>;
136
+ payment_handlers?: Record<string, { id: string; version: string; config?: Record<string, unknown> }[]>;
137
+ };
138
+ id: string;
139
+ status: CheckoutStatus;
140
+ messages?: CheckoutMessage[];
141
+ currency: string;
142
+ buyer?: Buyer;
143
+ line_items: LineItem[];
144
+ totals: { type: string; amount: number }[];
145
+ links: { type: string; url: string; title?: string }[];
146
+ fulfillment?: Fulfillment;
147
+ payment?: Payment;
148
+ continue_url?: string;
149
+ expires_at?: string;
150
+ order?: OrderConfirmation;
151
+ }
152
+
153
+ export interface Order {
154
+ ucp: {
155
+ version: string;
156
+ capabilities: Record<string, { version: string }[]>;
157
+ };
158
+ id: string;
159
+ checkout_id: string;
160
+ permalink_url?: string;
161
+ line_items: LineItem[];
162
+ buyer?: Buyer;
163
+ totals: { type: string; amount: number }[];
164
+ fulfillment?: {
165
+ expectations: {
166
+ id: string;
167
+ line_items: { id: string; quantity: number }[];
168
+ method_type: string;
169
+ destination?: Address;
170
+ description?: string;
171
+ fulfillable_on?: string;
172
+ }[];
173
+ events: {
174
+ id: string;
175
+ occurred_at: string;
176
+ type: string;
177
+ line_items: { id: string; quantity: number }[];
178
+ tracking_number?: string;
179
+ tracking_url?: string;
180
+ description?: string;
181
+ }[];
182
+ };
183
+ created_at: string;
184
+ }
185
+
186
+ // Request validation schemas
187
+ export const CreateCheckoutRequestSchema = z.object({
188
+ line_items: z.array(z.object({
189
+ id: z.string().optional(),
190
+ item: z.object({
191
+ id: z.string(),
192
+ title: z.string().optional(),
193
+ price: z.number().int().min(0).optional(),
194
+ }),
195
+ quantity: z.number().int().min(1).max(100),
196
+ })).min(1).max(50),
197
+ buyer: z.object({
198
+ email: z.string().email().optional(),
199
+ first_name: z.string().max(100).optional(),
200
+ last_name: z.string().max(100).optional(),
201
+ phone: z.string().max(30).optional(),
202
+ }).optional(),
203
+ fulfillment: z.object({
204
+ methods: z.array(z.object({
205
+ id: z.string().optional(),
206
+ type: z.enum(['shipping', 'pickup', 'digital']),
207
+ line_item_ids: z.array(z.string()).optional(),
208
+ selected_destination_id: z.string().optional(),
209
+ destinations: z.array(z.object({
210
+ id: z.string().optional(),
211
+ first_name: z.string().max(100).optional(),
212
+ last_name: z.string().max(100).optional(),
213
+ street_address: z.string().max(200),
214
+ address_locality: z.string().max(100),
215
+ address_region: z.string().max(100),
216
+ postal_code: z.string().max(20),
217
+ address_country: z.string().length(2),
218
+ })),
219
+ groups: z.array(z.any()).optional(),
220
+ })).optional(),
221
+ }).optional(),
222
+ });
223
+
224
+ export const UpdateCheckoutRequestSchema = z.object({
225
+ buyer: z.object({
226
+ email: z.string().email().optional(),
227
+ first_name: z.string().max(100).optional(),
228
+ last_name: z.string().max(100).optional(),
229
+ phone: z.string().max(30).optional(),
230
+ }).optional(),
231
+ line_items: z.array(z.object({
232
+ id: z.string().optional(),
233
+ item: z.object({
234
+ id: z.string(),
235
+ title: z.string().optional(),
236
+ price: z.number().int().min(0).optional(),
237
+ }),
238
+ quantity: z.number().int().min(1).max(100),
239
+ })).optional(),
240
+ fulfillment: z.any().optional(),
241
+ });
242
+
243
+ export type CreateCheckoutRequest = z.infer<typeof CreateCheckoutRequestSchema>;
244
+ export type UpdateCheckoutRequest = z.infer<typeof UpdateCheckoutRequestSchema>;
package/src/stripe.ts ADDED
@@ -0,0 +1,106 @@
1
+ import Stripe from 'stripe';
2
+ import { config } from './config';
3
+ import { logger } from './logger';
4
+
5
+ let stripeClient: Stripe | null = null;
6
+
7
+ export function getStripe(): Stripe | null {
8
+ if (!config.stripeSecretKey) {
9
+ return null;
10
+ }
11
+ if (!stripeClient) {
12
+ stripeClient = new Stripe(config.stripeSecretKey, {
13
+ apiVersion: '2026-01-28.clover',
14
+ });
15
+ logger.info('Stripe client initialized');
16
+ }
17
+ return stripeClient;
18
+ }
19
+
20
+ export function isStripeConfigured(): boolean {
21
+ return !!config.stripeSecretKey;
22
+ }
23
+
24
+ export interface CreatePaymentIntentParams {
25
+ amount: number; // in cents
26
+ currency: string;
27
+ checkoutId: string;
28
+ customerEmail?: string;
29
+ metadata?: Record<string, string>;
30
+ }
31
+
32
+ export async function createPaymentIntent(params: CreatePaymentIntentParams): Promise<Stripe.PaymentIntent | null> {
33
+ const stripe = getStripe();
34
+ if (!stripe) {
35
+ logger.warn('Stripe not configured, skipping payment intent creation');
36
+ return null;
37
+ }
38
+
39
+ try {
40
+ const paymentIntent = await stripe.paymentIntents.create({
41
+ amount: params.amount,
42
+ currency: params.currency.toLowerCase(),
43
+ metadata: {
44
+ checkout_id: params.checkoutId,
45
+ ...params.metadata,
46
+ },
47
+ receipt_email: params.customerEmail,
48
+ automatic_payment_methods: {
49
+ enabled: true,
50
+ },
51
+ }, {
52
+ idempotencyKey: `checkout_${params.checkoutId}`,
53
+ });
54
+
55
+ logger.info({
56
+ paymentIntentId: paymentIntent.id,
57
+ checkoutId: params.checkoutId,
58
+ amount: params.amount,
59
+ }, 'Payment intent created');
60
+
61
+ return paymentIntent;
62
+ } catch (error) {
63
+ logger.error({ error, checkoutId: params.checkoutId }, 'Failed to create payment intent');
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ export async function confirmPaymentIntent(paymentIntentId: string): Promise<Stripe.PaymentIntent | null> {
69
+ const stripe = getStripe();
70
+ if (!stripe) return null;
71
+
72
+ try {
73
+ const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
74
+ logger.info({ paymentIntentId, status: paymentIntent.status }, 'Payment intent retrieved');
75
+ return paymentIntent;
76
+ } catch (error) {
77
+ logger.error({ error, paymentIntentId }, 'Failed to retrieve payment intent');
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ export async function cancelPaymentIntent(paymentIntentId: string): Promise<Stripe.PaymentIntent | null> {
83
+ const stripe = getStripe();
84
+ if (!stripe) return null;
85
+
86
+ try {
87
+ const paymentIntent = await stripe.paymentIntents.cancel(paymentIntentId);
88
+ logger.info({ paymentIntentId }, 'Payment intent canceled');
89
+ return paymentIntent;
90
+ } catch (error) {
91
+ logger.error({ error, paymentIntentId }, 'Failed to cancel payment intent');
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ export function constructWebhookEvent(payload: string | Buffer, signature: string): Stripe.Event | null {
97
+ const stripe = getStripe();
98
+ if (!stripe || !config.stripeWebhookSecret) return null;
99
+
100
+ try {
101
+ return stripe.webhooks.constructEvent(payload, signature, config.stripeWebhookSecret);
102
+ } catch (error) {
103
+ logger.error({ error }, 'Webhook signature verification failed');
104
+ return null;
105
+ }
106
+ }