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.
- package/package.json +18 -18
- package/dist/__tests__/api.test.d.ts +0 -1
- package/dist/__tests__/api.test.js +0 -121
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -235
- package/dist/config.d.ts +0 -14
- package/dist/config.js +0 -22
- package/dist/db.d.ts +0 -4
- package/dist/db.js +0 -164
- package/dist/express-app.d.ts +0 -5
- package/dist/express-app.js +0 -495
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -12
- package/dist/logger.d.ts +0 -3
- package/dist/logger.js +0 -15
- package/dist/oauth.d.ts +0 -60
- package/dist/oauth.js +0 -211
- package/dist/paypal.d.ts +0 -20
- package/dist/paypal.js +0 -111
- package/dist/schema.d.ts +0 -287
- package/dist/schema.js +0 -113
- package/dist/stripe.d.ts +0 -14
- package/dist/stripe.js +0 -103
- package/dist/ucp-server-db.d.ts +0 -76
- package/dist/ucp-server-db.js +0 -369
- package/dist/ucp-server.d.ts +0 -83
- package/dist/ucp-server.js +0 -381
package/dist/schema.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.UpdateCheckoutRequestSchema = exports.CreateCheckoutRequestSchema = exports.MerchantConfigSchema = exports.OAuthConfigSchema = exports.PaymentHandlerSchema = exports.ShippingOptionSchema = exports.ItemSchema = exports.UCP_VERSION = void 0;
|
|
4
|
-
const zod_1 = require("zod");
|
|
5
|
-
// UCP Version
|
|
6
|
-
exports.UCP_VERSION = '2026-01-11';
|
|
7
|
-
// Product/Item Schema
|
|
8
|
-
exports.ItemSchema = zod_1.z.object({
|
|
9
|
-
id: zod_1.z.string(),
|
|
10
|
-
title: zod_1.z.string(),
|
|
11
|
-
description: zod_1.z.string().optional(),
|
|
12
|
-
price: zod_1.z.number().int().min(0), // Minor units (cents)
|
|
13
|
-
image_url: zod_1.z.string().url().optional(),
|
|
14
|
-
sku: zod_1.z.string().optional(),
|
|
15
|
-
stock: zod_1.z.number().int().min(0).optional(), // Inventory tracking
|
|
16
|
-
});
|
|
17
|
-
// Shipping Option Schema
|
|
18
|
-
exports.ShippingOptionSchema = zod_1.z.object({
|
|
19
|
-
id: zod_1.z.string(),
|
|
20
|
-
title: zod_1.z.string(),
|
|
21
|
-
description: zod_1.z.string().optional(),
|
|
22
|
-
price: zod_1.z.number().int().min(0), // Minor units
|
|
23
|
-
estimated_days: zod_1.z.string().optional(),
|
|
24
|
-
});
|
|
25
|
-
// Payment Handler Schema
|
|
26
|
-
exports.PaymentHandlerSchema = zod_1.z.object({
|
|
27
|
-
namespace: zod_1.z.string(), // e.g., "com.stripe", "com.google.pay"
|
|
28
|
-
id: zod_1.z.string(),
|
|
29
|
-
config: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
|
|
30
|
-
});
|
|
31
|
-
// OAuth Configuration Schema
|
|
32
|
-
exports.OAuthConfigSchema = zod_1.z.discriminatedUnion('provider', [
|
|
33
|
-
zod_1.z.object({
|
|
34
|
-
provider: zod_1.z.literal('built-in'),
|
|
35
|
-
}),
|
|
36
|
-
zod_1.z.object({
|
|
37
|
-
provider: zod_1.z.literal('external'),
|
|
38
|
-
issuer: zod_1.z.string().url(),
|
|
39
|
-
authorization_endpoint: zod_1.z.string().url(),
|
|
40
|
-
token_endpoint: zod_1.z.string().url(),
|
|
41
|
-
revocation_endpoint: zod_1.z.string().url().optional(),
|
|
42
|
-
jwks_uri: zod_1.z.string().url().optional(),
|
|
43
|
-
}),
|
|
44
|
-
]);
|
|
45
|
-
// Merchant Configuration Schema
|
|
46
|
-
exports.MerchantConfigSchema = zod_1.z.object({
|
|
47
|
-
name: zod_1.z.string(),
|
|
48
|
-
domain: zod_1.z.string().url(),
|
|
49
|
-
currency: zod_1.z.string().default('USD'),
|
|
50
|
-
terms_url: zod_1.z.string().url().optional(),
|
|
51
|
-
privacy_url: zod_1.z.string().url().optional(),
|
|
52
|
-
items: zod_1.z.array(exports.ItemSchema),
|
|
53
|
-
shipping_options: zod_1.z.array(exports.ShippingOptionSchema).default([]),
|
|
54
|
-
payment_handlers: zod_1.z.array(exports.PaymentHandlerSchema).default([]),
|
|
55
|
-
tax_rate: zod_1.z.number().min(0).max(1).default(0), // e.g., 0.08 for 8%
|
|
56
|
-
port: zod_1.z.number().default(3000),
|
|
57
|
-
oauth: exports.OAuthConfigSchema.optional(),
|
|
58
|
-
});
|
|
59
|
-
// Request validation schemas
|
|
60
|
-
exports.CreateCheckoutRequestSchema = zod_1.z.object({
|
|
61
|
-
line_items: zod_1.z.array(zod_1.z.object({
|
|
62
|
-
id: zod_1.z.string().optional(),
|
|
63
|
-
item: zod_1.z.object({
|
|
64
|
-
id: zod_1.z.string(),
|
|
65
|
-
title: zod_1.z.string().optional(),
|
|
66
|
-
price: zod_1.z.number().int().min(0).optional(),
|
|
67
|
-
}),
|
|
68
|
-
quantity: zod_1.z.number().int().min(1).max(100),
|
|
69
|
-
})).min(1).max(50),
|
|
70
|
-
buyer: zod_1.z.object({
|
|
71
|
-
email: zod_1.z.string().email().optional(),
|
|
72
|
-
first_name: zod_1.z.string().max(100).optional(),
|
|
73
|
-
last_name: zod_1.z.string().max(100).optional(),
|
|
74
|
-
phone: zod_1.z.string().max(30).optional(),
|
|
75
|
-
}).optional(),
|
|
76
|
-
fulfillment: zod_1.z.object({
|
|
77
|
-
methods: zod_1.z.array(zod_1.z.object({
|
|
78
|
-
id: zod_1.z.string().optional(),
|
|
79
|
-
type: zod_1.z.enum(['shipping', 'pickup', 'digital']),
|
|
80
|
-
line_item_ids: zod_1.z.array(zod_1.z.string()).optional(),
|
|
81
|
-
selected_destination_id: zod_1.z.string().optional(),
|
|
82
|
-
destinations: zod_1.z.array(zod_1.z.object({
|
|
83
|
-
id: zod_1.z.string().optional(),
|
|
84
|
-
first_name: zod_1.z.string().max(100).optional(),
|
|
85
|
-
last_name: zod_1.z.string().max(100).optional(),
|
|
86
|
-
street_address: zod_1.z.string().max(200),
|
|
87
|
-
address_locality: zod_1.z.string().max(100),
|
|
88
|
-
address_region: zod_1.z.string().max(100),
|
|
89
|
-
postal_code: zod_1.z.string().max(20),
|
|
90
|
-
address_country: zod_1.z.string().length(2),
|
|
91
|
-
})),
|
|
92
|
-
groups: zod_1.z.array(zod_1.z.any()).optional(),
|
|
93
|
-
})).optional(),
|
|
94
|
-
}).optional(),
|
|
95
|
-
});
|
|
96
|
-
exports.UpdateCheckoutRequestSchema = zod_1.z.object({
|
|
97
|
-
buyer: zod_1.z.object({
|
|
98
|
-
email: zod_1.z.string().email().optional(),
|
|
99
|
-
first_name: zod_1.z.string().max(100).optional(),
|
|
100
|
-
last_name: zod_1.z.string().max(100).optional(),
|
|
101
|
-
phone: zod_1.z.string().max(30).optional(),
|
|
102
|
-
}).optional(),
|
|
103
|
-
line_items: zod_1.z.array(zod_1.z.object({
|
|
104
|
-
id: zod_1.z.string().optional(),
|
|
105
|
-
item: zod_1.z.object({
|
|
106
|
-
id: zod_1.z.string(),
|
|
107
|
-
title: zod_1.z.string().optional(),
|
|
108
|
-
price: zod_1.z.number().int().min(0).optional(),
|
|
109
|
-
}),
|
|
110
|
-
quantity: zod_1.z.number().int().min(1).max(100),
|
|
111
|
-
})).optional(),
|
|
112
|
-
fulfillment: zod_1.z.any().optional(),
|
|
113
|
-
});
|
package/dist/stripe.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import Stripe from 'stripe';
|
|
2
|
-
export declare function getStripe(): Stripe | null;
|
|
3
|
-
export declare function isStripeConfigured(): boolean;
|
|
4
|
-
export interface CreatePaymentIntentParams {
|
|
5
|
-
amount: number;
|
|
6
|
-
currency: string;
|
|
7
|
-
checkoutId: string;
|
|
8
|
-
customerEmail?: string;
|
|
9
|
-
metadata?: Record<string, string>;
|
|
10
|
-
}
|
|
11
|
-
export declare function createPaymentIntent(params: CreatePaymentIntentParams): Promise<Stripe.PaymentIntent | null>;
|
|
12
|
-
export declare function confirmPaymentIntent(paymentIntentId: string): Promise<Stripe.PaymentIntent | null>;
|
|
13
|
-
export declare function cancelPaymentIntent(paymentIntentId: string): Promise<Stripe.PaymentIntent | null>;
|
|
14
|
-
export declare function constructWebhookEvent(payload: string | Buffer, signature: string): Stripe.Event | null;
|
package/dist/stripe.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getStripe = getStripe;
|
|
7
|
-
exports.isStripeConfigured = isStripeConfigured;
|
|
8
|
-
exports.createPaymentIntent = createPaymentIntent;
|
|
9
|
-
exports.confirmPaymentIntent = confirmPaymentIntent;
|
|
10
|
-
exports.cancelPaymentIntent = cancelPaymentIntent;
|
|
11
|
-
exports.constructWebhookEvent = constructWebhookEvent;
|
|
12
|
-
const stripe_1 = __importDefault(require("stripe"));
|
|
13
|
-
const config_1 = require("./config");
|
|
14
|
-
const logger_1 = require("./logger");
|
|
15
|
-
let stripeClient = null;
|
|
16
|
-
function getStripe() {
|
|
17
|
-
if (!config_1.config.stripeSecretKey) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
if (!stripeClient) {
|
|
21
|
-
stripeClient = new stripe_1.default(config_1.config.stripeSecretKey, {
|
|
22
|
-
apiVersion: '2026-01-28.clover',
|
|
23
|
-
});
|
|
24
|
-
logger_1.logger.info('Stripe client initialized');
|
|
25
|
-
}
|
|
26
|
-
return stripeClient;
|
|
27
|
-
}
|
|
28
|
-
function isStripeConfigured() {
|
|
29
|
-
return !!config_1.config.stripeSecretKey;
|
|
30
|
-
}
|
|
31
|
-
async function createPaymentIntent(params) {
|
|
32
|
-
const stripe = getStripe();
|
|
33
|
-
if (!stripe) {
|
|
34
|
-
logger_1.logger.warn('Stripe not configured, skipping payment intent creation');
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
try {
|
|
38
|
-
const paymentIntent = await stripe.paymentIntents.create({
|
|
39
|
-
amount: params.amount,
|
|
40
|
-
currency: params.currency.toLowerCase(),
|
|
41
|
-
metadata: {
|
|
42
|
-
checkout_id: params.checkoutId,
|
|
43
|
-
...params.metadata,
|
|
44
|
-
},
|
|
45
|
-
receipt_email: params.customerEmail,
|
|
46
|
-
automatic_payment_methods: {
|
|
47
|
-
enabled: true,
|
|
48
|
-
},
|
|
49
|
-
}, {
|
|
50
|
-
idempotencyKey: `checkout_${params.checkoutId}`,
|
|
51
|
-
});
|
|
52
|
-
logger_1.logger.info({
|
|
53
|
-
paymentIntentId: paymentIntent.id,
|
|
54
|
-
checkoutId: params.checkoutId,
|
|
55
|
-
amount: params.amount,
|
|
56
|
-
}, 'Payment intent created');
|
|
57
|
-
return paymentIntent;
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
logger_1.logger.error({ error, checkoutId: params.checkoutId }, 'Failed to create payment intent');
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
async function confirmPaymentIntent(paymentIntentId) {
|
|
65
|
-
const stripe = getStripe();
|
|
66
|
-
if (!stripe)
|
|
67
|
-
return null;
|
|
68
|
-
try {
|
|
69
|
-
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
|
|
70
|
-
logger_1.logger.info({ paymentIntentId, status: paymentIntent.status }, 'Payment intent retrieved');
|
|
71
|
-
return paymentIntent;
|
|
72
|
-
}
|
|
73
|
-
catch (error) {
|
|
74
|
-
logger_1.logger.error({ error, paymentIntentId }, 'Failed to retrieve payment intent');
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
async function cancelPaymentIntent(paymentIntentId) {
|
|
79
|
-
const stripe = getStripe();
|
|
80
|
-
if (!stripe)
|
|
81
|
-
return null;
|
|
82
|
-
try {
|
|
83
|
-
const paymentIntent = await stripe.paymentIntents.cancel(paymentIntentId);
|
|
84
|
-
logger_1.logger.info({ paymentIntentId }, 'Payment intent canceled');
|
|
85
|
-
return paymentIntent;
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
logger_1.logger.error({ error, paymentIntentId }, 'Failed to cancel payment intent');
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
function constructWebhookEvent(payload, signature) {
|
|
93
|
-
const stripe = getStripe();
|
|
94
|
-
if (!stripe || !config_1.config.stripeWebhookSecret)
|
|
95
|
-
return null;
|
|
96
|
-
try {
|
|
97
|
-
return stripe.webhooks.constructEvent(payload, signature, config_1.config.stripeWebhookSecret);
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
logger_1.logger.error({ error }, 'Webhook signature verification failed');
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
}
|
package/dist/ucp-server-db.d.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { MerchantConfig, CheckoutSession, Order, Buyer, Fulfillment } from './schema';
|
|
2
|
-
export declare class UCPServerDB {
|
|
3
|
-
private config;
|
|
4
|
-
private itemsMap;
|
|
5
|
-
constructor(config: MerchantConfig);
|
|
6
|
-
getProfile(): {
|
|
7
|
-
ucp: {
|
|
8
|
-
version: string;
|
|
9
|
-
services: {
|
|
10
|
-
'dev.ucp.shopping': {
|
|
11
|
-
version: string;
|
|
12
|
-
spec: string;
|
|
13
|
-
transport: string;
|
|
14
|
-
endpoint: string;
|
|
15
|
-
schema: string;
|
|
16
|
-
}[];
|
|
17
|
-
};
|
|
18
|
-
capabilities: {
|
|
19
|
-
'dev.ucp.identity_linking'?: {
|
|
20
|
-
version: string;
|
|
21
|
-
spec: string;
|
|
22
|
-
}[] | undefined;
|
|
23
|
-
'dev.ucp.shopping.checkout': {
|
|
24
|
-
version: string;
|
|
25
|
-
spec: string;
|
|
26
|
-
schema: string;
|
|
27
|
-
}[];
|
|
28
|
-
'dev.ucp.shopping.fulfillment': {
|
|
29
|
-
version: string;
|
|
30
|
-
spec: string;
|
|
31
|
-
schema: string;
|
|
32
|
-
extends: string;
|
|
33
|
-
}[];
|
|
34
|
-
'dev.ucp.shopping.order': {
|
|
35
|
-
version: string;
|
|
36
|
-
spec: string;
|
|
37
|
-
schema: string;
|
|
38
|
-
}[];
|
|
39
|
-
};
|
|
40
|
-
payment_handlers: Record<string, {
|
|
41
|
-
id: string;
|
|
42
|
-
version: string;
|
|
43
|
-
config?: Record<string, unknown>;
|
|
44
|
-
}[]>;
|
|
45
|
-
};
|
|
46
|
-
};
|
|
47
|
-
private buildLinks;
|
|
48
|
-
private buildPaymentHandlers;
|
|
49
|
-
private validateCheckout;
|
|
50
|
-
private calculateTotals;
|
|
51
|
-
private sessionFromRow;
|
|
52
|
-
createCheckout(data: {
|
|
53
|
-
line_items: {
|
|
54
|
-
id?: string;
|
|
55
|
-
item: {
|
|
56
|
-
id: string;
|
|
57
|
-
title?: string;
|
|
58
|
-
price?: number;
|
|
59
|
-
};
|
|
60
|
-
quantity: number;
|
|
61
|
-
}[];
|
|
62
|
-
}): CheckoutSession;
|
|
63
|
-
getCheckout(id: string): CheckoutSession | undefined;
|
|
64
|
-
updateCheckout(id: string, data: {
|
|
65
|
-
buyer?: Buyer;
|
|
66
|
-
line_items?: any[];
|
|
67
|
-
fulfillment?: Fulfillment;
|
|
68
|
-
}): CheckoutSession | undefined;
|
|
69
|
-
completeCheckout(id: string): Promise<CheckoutSession | {
|
|
70
|
-
error: string;
|
|
71
|
-
}>;
|
|
72
|
-
cancelCheckout(id: string): CheckoutSession | undefined;
|
|
73
|
-
getOrder(id: string): Order | undefined;
|
|
74
|
-
listOrders(): Order[];
|
|
75
|
-
getConfig(): MerchantConfig;
|
|
76
|
-
}
|
package/dist/ucp-server-db.js
DELETED
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.UCPServerDB = void 0;
|
|
4
|
-
const uuid_1 = require("uuid");
|
|
5
|
-
const db_1 = require("./db");
|
|
6
|
-
const logger_1 = require("./logger");
|
|
7
|
-
const stripe_1 = require("./stripe");
|
|
8
|
-
const paypal_1 = require("./paypal");
|
|
9
|
-
const schema_1 = require("./schema");
|
|
10
|
-
class UCPServerDB {
|
|
11
|
-
config;
|
|
12
|
-
itemsMap;
|
|
13
|
-
constructor(config) {
|
|
14
|
-
this.config = config;
|
|
15
|
-
this.itemsMap = new Map(config.items.map((item) => [item.id, item]));
|
|
16
|
-
}
|
|
17
|
-
getProfile() {
|
|
18
|
-
const paymentHandlers = {};
|
|
19
|
-
for (const handler of this.config.payment_handlers) {
|
|
20
|
-
paymentHandlers[handler.namespace] = [
|
|
21
|
-
{ id: handler.id, version: schema_1.UCP_VERSION, config: handler.config },
|
|
22
|
-
];
|
|
23
|
-
}
|
|
24
|
-
return {
|
|
25
|
-
ucp: {
|
|
26
|
-
version: schema_1.UCP_VERSION,
|
|
27
|
-
services: {
|
|
28
|
-
'dev.ucp.shopping': [
|
|
29
|
-
{
|
|
30
|
-
version: schema_1.UCP_VERSION,
|
|
31
|
-
spec: 'https://ucp.dev/specification/overview',
|
|
32
|
-
transport: 'rest',
|
|
33
|
-
endpoint: `${this.config.domain}/ucp/v1`,
|
|
34
|
-
schema: 'https://ucp.dev/services/shopping/rest.openapi.json',
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
},
|
|
38
|
-
capabilities: {
|
|
39
|
-
'dev.ucp.shopping.checkout': [
|
|
40
|
-
{ version: schema_1.UCP_VERSION, spec: 'https://ucp.dev/specification/checkout', schema: 'https://ucp.dev/schemas/shopping/checkout.json' },
|
|
41
|
-
],
|
|
42
|
-
'dev.ucp.shopping.fulfillment': [
|
|
43
|
-
{ version: schema_1.UCP_VERSION, spec: 'https://ucp.dev/specification/fulfillment', schema: 'https://ucp.dev/schemas/shopping/fulfillment.json', extends: 'dev.ucp.shopping.checkout' },
|
|
44
|
-
],
|
|
45
|
-
'dev.ucp.shopping.order': [
|
|
46
|
-
{ version: schema_1.UCP_VERSION, spec: 'https://ucp.dev/specification/order', schema: 'https://ucp.dev/schemas/shopping/order.json' },
|
|
47
|
-
],
|
|
48
|
-
...(this.config.oauth ? {
|
|
49
|
-
'dev.ucp.identity_linking': [
|
|
50
|
-
{ version: schema_1.UCP_VERSION, spec: 'https://ucp.dev/specification/identity-linking' },
|
|
51
|
-
],
|
|
52
|
-
} : {}),
|
|
53
|
-
},
|
|
54
|
-
payment_handlers: paymentHandlers,
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
buildLinks() {
|
|
59
|
-
const links = [];
|
|
60
|
-
if (this.config.terms_url)
|
|
61
|
-
links.push({ type: 'terms_of_service', url: this.config.terms_url });
|
|
62
|
-
if (this.config.privacy_url)
|
|
63
|
-
links.push({ type: 'privacy_policy', url: this.config.privacy_url });
|
|
64
|
-
return links;
|
|
65
|
-
}
|
|
66
|
-
buildPaymentHandlers() {
|
|
67
|
-
const paymentHandlers = {};
|
|
68
|
-
for (const handler of this.config.payment_handlers) {
|
|
69
|
-
paymentHandlers[handler.namespace] = [{ id: handler.id, version: schema_1.UCP_VERSION, config: handler.config }];
|
|
70
|
-
}
|
|
71
|
-
return paymentHandlers;
|
|
72
|
-
}
|
|
73
|
-
validateCheckout(session) {
|
|
74
|
-
const messages = [];
|
|
75
|
-
if (!session.buyer?.email) {
|
|
76
|
-
messages.push({ type: 'error', code: 'missing', path: '$.buyer.email', content: 'Buyer email is required', severity: 'recoverable' });
|
|
77
|
-
}
|
|
78
|
-
if (!session.fulfillment?.methods?.[0]?.selected_destination_id) {
|
|
79
|
-
messages.push({ type: 'error', code: 'missing', path: '$.fulfillment.methods[0].selected_destination_id', content: 'Shipping address is required', severity: 'recoverable' });
|
|
80
|
-
}
|
|
81
|
-
return messages;
|
|
82
|
-
}
|
|
83
|
-
calculateTotals(lineItems, shippingTotal = 0) {
|
|
84
|
-
const subtotal = lineItems.reduce((sum, li) => sum + li.item.price * li.quantity, 0);
|
|
85
|
-
const tax = Math.round(subtotal * this.config.tax_rate);
|
|
86
|
-
const totals = [
|
|
87
|
-
{ type: 'subtotal', amount: subtotal },
|
|
88
|
-
{ type: 'tax', amount: tax },
|
|
89
|
-
{ type: 'total', amount: subtotal + shippingTotal + tax },
|
|
90
|
-
];
|
|
91
|
-
if (shippingTotal > 0) {
|
|
92
|
-
totals.splice(1, 0, { type: 'shipping', amount: shippingTotal });
|
|
93
|
-
}
|
|
94
|
-
return totals;
|
|
95
|
-
}
|
|
96
|
-
sessionFromRow(row) {
|
|
97
|
-
if (!row)
|
|
98
|
-
return undefined;
|
|
99
|
-
const lineItems = db_1.db.prepare('SELECT * FROM line_items WHERE checkout_id = ?').all(row.id);
|
|
100
|
-
return {
|
|
101
|
-
ucp: {
|
|
102
|
-
version: schema_1.UCP_VERSION,
|
|
103
|
-
capabilities: { 'dev.ucp.shopping.checkout': [{ version: schema_1.UCP_VERSION }] },
|
|
104
|
-
payment_handlers: this.buildPaymentHandlers(),
|
|
105
|
-
},
|
|
106
|
-
id: row.id,
|
|
107
|
-
status: row.status,
|
|
108
|
-
currency: row.currency,
|
|
109
|
-
buyer: row.buyer_json ? JSON.parse(row.buyer_json) : undefined,
|
|
110
|
-
line_items: lineItems.map((li) => ({
|
|
111
|
-
id: li.id,
|
|
112
|
-
item: JSON.parse(li.item_json),
|
|
113
|
-
quantity: li.quantity,
|
|
114
|
-
totals: li.totals_json ? JSON.parse(li.totals_json) : undefined,
|
|
115
|
-
})),
|
|
116
|
-
totals: JSON.parse(row.totals_json),
|
|
117
|
-
links: JSON.parse(row.links_json),
|
|
118
|
-
fulfillment: row.fulfillment_json ? JSON.parse(row.fulfillment_json) : undefined,
|
|
119
|
-
payment: row.payment_json ? JSON.parse(row.payment_json) : undefined,
|
|
120
|
-
messages: row.messages_json ? JSON.parse(row.messages_json) : undefined,
|
|
121
|
-
continue_url: row.continue_url || undefined,
|
|
122
|
-
expires_at: row.expires_at || undefined,
|
|
123
|
-
order: row.order_json ? JSON.parse(row.order_json) : undefined,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
createCheckout(data) {
|
|
127
|
-
const id = `chk_${(0, uuid_1.v4)().replace(/-/g, '').slice(0, 12)}`;
|
|
128
|
-
const now = new Date().toISOString();
|
|
129
|
-
const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
|
130
|
-
const lineItems = data.line_items.map((li, idx) => {
|
|
131
|
-
const catalogItem = this.itemsMap.get(li.item.id);
|
|
132
|
-
const item = catalogItem || { id: li.item.id, title: li.item.title || 'Unknown Item', price: li.item.price || 0 };
|
|
133
|
-
const subtotal = item.price * li.quantity;
|
|
134
|
-
return {
|
|
135
|
-
id: li.id || `li_${idx + 1}`,
|
|
136
|
-
item,
|
|
137
|
-
quantity: li.quantity,
|
|
138
|
-
totals: [{ type: 'subtotal', amount: subtotal }, { type: 'total', amount: subtotal }],
|
|
139
|
-
};
|
|
140
|
-
});
|
|
141
|
-
const totals = this.calculateTotals(lineItems);
|
|
142
|
-
const links = this.buildLinks();
|
|
143
|
-
const messages = this.validateCheckout({});
|
|
144
|
-
const status = messages.length > 0 ? 'incomplete' : 'ready_for_complete';
|
|
145
|
-
const continueUrl = `${this.config.domain}/checkout/${id}`;
|
|
146
|
-
const insertCheckout = db_1.db.prepare(`
|
|
147
|
-
INSERT INTO checkouts (id, status, currency, totals_json, links_json, messages_json, continue_url, expires_at, created_at, updated_at)
|
|
148
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
149
|
-
`);
|
|
150
|
-
const insertLineItem = db_1.db.prepare(`
|
|
151
|
-
INSERT INTO line_items (id, checkout_id, item_json, quantity, totals_json)
|
|
152
|
-
VALUES (?, ?, ?, ?, ?)
|
|
153
|
-
`);
|
|
154
|
-
db_1.db.transaction(() => {
|
|
155
|
-
insertCheckout.run(id, status, this.config.currency, JSON.stringify(totals), JSON.stringify(links), messages.length > 0 ? JSON.stringify(messages) : null, continueUrl, expiresAt, now, now);
|
|
156
|
-
for (const li of lineItems) {
|
|
157
|
-
insertLineItem.run(li.id, id, JSON.stringify(li.item), li.quantity, JSON.stringify(li.totals));
|
|
158
|
-
}
|
|
159
|
-
})();
|
|
160
|
-
logger_1.logger.info({ checkoutId: id, lineItems: lineItems.length }, 'Checkout created');
|
|
161
|
-
return this.getCheckout(id);
|
|
162
|
-
}
|
|
163
|
-
getCheckout(id) {
|
|
164
|
-
const row = db_1.db.prepare('SELECT * FROM checkouts WHERE id = ?').get(id);
|
|
165
|
-
return this.sessionFromRow(row);
|
|
166
|
-
}
|
|
167
|
-
updateCheckout(id, data) {
|
|
168
|
-
const session = this.getCheckout(id);
|
|
169
|
-
if (!session)
|
|
170
|
-
return undefined;
|
|
171
|
-
const now = new Date().toISOString();
|
|
172
|
-
let buyer = session.buyer;
|
|
173
|
-
let lineItems = session.line_items;
|
|
174
|
-
let fulfillment = session.fulfillment;
|
|
175
|
-
if (data.buyer) {
|
|
176
|
-
buyer = { ...buyer, ...data.buyer };
|
|
177
|
-
}
|
|
178
|
-
if (data.line_items) {
|
|
179
|
-
lineItems = data.line_items.map((li, idx) => {
|
|
180
|
-
const catalogItem = this.itemsMap.get(li.item.id);
|
|
181
|
-
const item = catalogItem || { id: li.item.id, title: li.item.title || 'Unknown Item', price: li.item.price || 0 };
|
|
182
|
-
const subtotal = item.price * li.quantity;
|
|
183
|
-
return {
|
|
184
|
-
id: li.id || `li_${idx + 1}`,
|
|
185
|
-
item,
|
|
186
|
-
quantity: li.quantity,
|
|
187
|
-
totals: [{ type: 'subtotal', amount: subtotal }, { type: 'total', amount: subtotal }],
|
|
188
|
-
};
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
if (data.fulfillment) {
|
|
192
|
-
const methods = data.fulfillment.methods.map((method, mIdx) => {
|
|
193
|
-
const destinations = method.destinations.map((dest, dIdx) => ({ ...dest, id: dest.id || `dest_${dIdx + 1}` }));
|
|
194
|
-
const groups = method.groups?.length ? method.groups : [{
|
|
195
|
-
id: `group_${mIdx + 1}`,
|
|
196
|
-
line_item_ids: lineItems.map((li) => li.id),
|
|
197
|
-
selected_option_id: this.config.shipping_options[0]?.id,
|
|
198
|
-
options: this.config.shipping_options.map((opt) => ({
|
|
199
|
-
id: opt.id, title: opt.title, description: opt.description || opt.estimated_days,
|
|
200
|
-
totals: [{ type: 'total', amount: opt.price }],
|
|
201
|
-
})),
|
|
202
|
-
}];
|
|
203
|
-
return {
|
|
204
|
-
id: method.id || `method_${mIdx + 1}`,
|
|
205
|
-
type: method.type,
|
|
206
|
-
line_item_ids: method.line_item_ids || lineItems.map((li) => li.id),
|
|
207
|
-
selected_destination_id: method.selected_destination_id || destinations[0]?.id,
|
|
208
|
-
destinations,
|
|
209
|
-
groups,
|
|
210
|
-
};
|
|
211
|
-
});
|
|
212
|
-
fulfillment = { methods };
|
|
213
|
-
}
|
|
214
|
-
// Calculate shipping total
|
|
215
|
-
let shippingTotal = 0;
|
|
216
|
-
if (fulfillment) {
|
|
217
|
-
for (const method of fulfillment.methods) {
|
|
218
|
-
for (const group of method.groups) {
|
|
219
|
-
const selectedOption = group.options.find((o) => o.id === group.selected_option_id);
|
|
220
|
-
if (selectedOption) {
|
|
221
|
-
const total = selectedOption.totals.find((t) => t.type === 'total');
|
|
222
|
-
shippingTotal += total?.amount || 0;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
const totals = this.calculateTotals(lineItems, shippingTotal);
|
|
228
|
-
const messages = this.validateCheckout({ buyer, fulfillment });
|
|
229
|
-
const status = messages.length > 0 ? 'incomplete' : 'ready_for_complete';
|
|
230
|
-
db_1.db.transaction(() => {
|
|
231
|
-
db_1.db.prepare(`
|
|
232
|
-
UPDATE checkouts SET status = ?, buyer_json = ?, fulfillment_json = ?, totals_json = ?, messages_json = ?, updated_at = ?
|
|
233
|
-
WHERE id = ?
|
|
234
|
-
`).run(status, buyer ? JSON.stringify(buyer) : null, fulfillment ? JSON.stringify(fulfillment) : null, JSON.stringify(totals), messages.length > 0 ? JSON.stringify(messages) : null, now, id);
|
|
235
|
-
db_1.db.prepare('DELETE FROM line_items WHERE checkout_id = ?').run(id);
|
|
236
|
-
const insertLineItem = db_1.db.prepare('INSERT INTO line_items (id, checkout_id, item_json, quantity, totals_json) VALUES (?, ?, ?, ?, ?)');
|
|
237
|
-
for (const li of lineItems) {
|
|
238
|
-
insertLineItem.run(li.id, id, JSON.stringify(li.item), li.quantity, JSON.stringify(li.totals));
|
|
239
|
-
}
|
|
240
|
-
})();
|
|
241
|
-
logger_1.logger.info({ checkoutId: id, status }, 'Checkout updated');
|
|
242
|
-
return this.getCheckout(id);
|
|
243
|
-
}
|
|
244
|
-
async completeCheckout(id) {
|
|
245
|
-
const session = this.getCheckout(id);
|
|
246
|
-
if (!session)
|
|
247
|
-
return { error: 'Checkout session not found' };
|
|
248
|
-
if (session.status !== 'ready_for_complete') {
|
|
249
|
-
return { error: 'Checkout is not ready for completion', messages: session.messages };
|
|
250
|
-
}
|
|
251
|
-
const orderId = `order_${(0, uuid_1.v4)().replace(/-/g, '').slice(0, 12)}`;
|
|
252
|
-
const now = new Date().toISOString();
|
|
253
|
-
// Get total amount for payment
|
|
254
|
-
const totalAmount = session.totals.find(t => t.type === 'total')?.amount || 0;
|
|
255
|
-
// Payment processing - try Stripe first, then PayPal
|
|
256
|
-
let paymentIntentId = null;
|
|
257
|
-
let paypalOrderId = null;
|
|
258
|
-
let paymentProvider = null;
|
|
259
|
-
let paymentStatus = 'none';
|
|
260
|
-
if (totalAmount > 0) {
|
|
261
|
-
// Try Stripe first
|
|
262
|
-
if ((0, stripe_1.isStripeConfigured)()) {
|
|
263
|
-
try {
|
|
264
|
-
const paymentIntent = await (0, stripe_1.createPaymentIntent)({
|
|
265
|
-
amount: totalAmount,
|
|
266
|
-
currency: session.currency,
|
|
267
|
-
checkoutId: id,
|
|
268
|
-
customerEmail: session.buyer?.email,
|
|
269
|
-
metadata: { order_id: orderId },
|
|
270
|
-
});
|
|
271
|
-
if (paymentIntent) {
|
|
272
|
-
paymentIntentId = paymentIntent.id;
|
|
273
|
-
paymentProvider = 'stripe';
|
|
274
|
-
paymentStatus = 'pending';
|
|
275
|
-
logger_1.logger.info({ checkoutId: id, paymentIntentId, amount: totalAmount }, 'Stripe payment intent created');
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
catch (error) {
|
|
279
|
-
logger_1.logger.error({ error, checkoutId: id }, 'Failed to create Stripe payment intent');
|
|
280
|
-
return { error: 'Payment processing failed' };
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
// Fall back to PayPal if Stripe not configured
|
|
284
|
-
else if ((0, paypal_1.isPayPalConfigured)()) {
|
|
285
|
-
try {
|
|
286
|
-
const ppOrder = await (0, paypal_1.createPayPalOrder)({
|
|
287
|
-
amount: totalAmount,
|
|
288
|
-
currency: session.currency,
|
|
289
|
-
checkoutId: id,
|
|
290
|
-
description: `Order ${orderId}`,
|
|
291
|
-
});
|
|
292
|
-
if (ppOrder) {
|
|
293
|
-
paypalOrderId = ppOrder.id;
|
|
294
|
-
paymentProvider = 'paypal';
|
|
295
|
-
paymentStatus = 'pending';
|
|
296
|
-
logger_1.logger.info({ checkoutId: id, paypalOrderId, amount: totalAmount }, 'PayPal order created');
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
catch (error) {
|
|
300
|
-
logger_1.logger.error({ error, checkoutId: id }, 'Failed to create PayPal order');
|
|
301
|
-
return { error: 'Payment processing failed' };
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
const orderFulfillment = session.fulfillment ? {
|
|
306
|
-
expectations: session.fulfillment.methods.map((m, idx) => ({
|
|
307
|
-
id: `exp_${idx + 1}`,
|
|
308
|
-
line_items: m.line_item_ids.map((liId) => {
|
|
309
|
-
const li = session.line_items.find((l) => l.id === liId);
|
|
310
|
-
return { id: liId, quantity: li?.quantity || 1 };
|
|
311
|
-
}),
|
|
312
|
-
method_type: m.type,
|
|
313
|
-
destination: m.destinations.find((d) => d.id === m.selected_destination_id),
|
|
314
|
-
description: 'Arrives in 5-7 business days',
|
|
315
|
-
fulfillable_on: 'now',
|
|
316
|
-
})),
|
|
317
|
-
events: [],
|
|
318
|
-
} : undefined;
|
|
319
|
-
const orderConfirmation = {
|
|
320
|
-
id: orderId,
|
|
321
|
-
permalink_url: `${this.config.domain}/orders/${orderId}`,
|
|
322
|
-
};
|
|
323
|
-
db_1.db.transaction(() => {
|
|
324
|
-
db_1.db.prepare(`
|
|
325
|
-
INSERT INTO orders (id, checkout_id, permalink_url, buyer_json, line_items_json, totals_json, fulfillment_json, payment_intent_id, paypal_order_id, payment_provider, payment_status, created_at)
|
|
326
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
327
|
-
`).run(orderId, id, orderConfirmation.permalink_url, session.buyer ? JSON.stringify(session.buyer) : null, JSON.stringify(session.line_items), JSON.stringify(session.totals), orderFulfillment ? JSON.stringify(orderFulfillment) : null, paymentIntentId, paypalOrderId, paymentProvider, paymentStatus, now);
|
|
328
|
-
db_1.db.prepare(`
|
|
329
|
-
UPDATE checkouts SET status = 'completed', order_json = ?, continue_url = NULL, messages_json = NULL, payment_intent_id = ?, paypal_order_id = ?, payment_provider = ?, payment_status = ?, updated_at = ?
|
|
330
|
-
WHERE id = ?
|
|
331
|
-
`).run(JSON.stringify(orderConfirmation), paymentIntentId, paypalOrderId, paymentProvider, paymentStatus, now, id);
|
|
332
|
-
})();
|
|
333
|
-
logger_1.logger.info({ checkoutId: id, orderId, paymentProvider, paymentIntentId, paypalOrderId }, 'Checkout completed, order created');
|
|
334
|
-
return this.getCheckout(id);
|
|
335
|
-
}
|
|
336
|
-
cancelCheckout(id) {
|
|
337
|
-
const session = this.getCheckout(id);
|
|
338
|
-
if (!session)
|
|
339
|
-
return undefined;
|
|
340
|
-
db_1.db.prepare(`UPDATE checkouts SET status = 'canceled', continue_url = NULL, updated_at = ? WHERE id = ?`)
|
|
341
|
-
.run(new Date().toISOString(), id);
|
|
342
|
-
logger_1.logger.info({ checkoutId: id }, 'Checkout canceled');
|
|
343
|
-
return this.getCheckout(id);
|
|
344
|
-
}
|
|
345
|
-
getOrder(id) {
|
|
346
|
-
const row = db_1.db.prepare('SELECT * FROM orders WHERE id = ?').get(id);
|
|
347
|
-
if (!row)
|
|
348
|
-
return undefined;
|
|
349
|
-
return {
|
|
350
|
-
ucp: { version: schema_1.UCP_VERSION, capabilities: { 'dev.ucp.shopping.order': [{ version: schema_1.UCP_VERSION }] } },
|
|
351
|
-
id: row.id,
|
|
352
|
-
checkout_id: row.checkout_id,
|
|
353
|
-
permalink_url: row.permalink_url,
|
|
354
|
-
line_items: JSON.parse(row.line_items_json),
|
|
355
|
-
buyer: row.buyer_json ? JSON.parse(row.buyer_json) : undefined,
|
|
356
|
-
totals: JSON.parse(row.totals_json),
|
|
357
|
-
fulfillment: row.fulfillment_json ? JSON.parse(row.fulfillment_json) : undefined,
|
|
358
|
-
created_at: row.created_at,
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
listOrders() {
|
|
362
|
-
const rows = db_1.db.prepare('SELECT * FROM orders ORDER BY created_at DESC').all();
|
|
363
|
-
return rows.map((row) => this.getOrder(row.id));
|
|
364
|
-
}
|
|
365
|
-
getConfig() {
|
|
366
|
-
return this.config;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
exports.UCPServerDB = UCPServerDB;
|