@portel/photon-core 1.5.0 → 2.1.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/dist/auto-ui.js +1 -1
- package/dist/auto-ui.js.map +1 -1
- package/dist/base.d.ts +1 -1
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +2 -2
- package/dist/base.js.map +1 -1
- package/dist/cli-ui-renderer.js +1 -1
- package/dist/cli-ui-renderer.js.map +1 -1
- package/dist/design-system/index.d.ts +21 -0
- package/dist/design-system/index.d.ts.map +1 -0
- package/dist/design-system/index.js +27 -0
- package/dist/design-system/index.js.map +1 -0
- package/dist/design-system/tokens.d.ts +149 -0
- package/dist/design-system/tokens.d.ts.map +1 -0
- package/dist/design-system/tokens.js +413 -0
- package/dist/design-system/tokens.js.map +1 -0
- package/dist/design-system/transaction-ui.d.ts +70 -0
- package/dist/design-system/transaction-ui.d.ts.map +1 -0
- package/dist/design-system/transaction-ui.js +982 -0
- package/dist/design-system/transaction-ui.js.map +1 -0
- package/dist/generator.d.ts +56 -6
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +6 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -56
- package/dist/index.js.map +1 -1
- package/dist/io.d.ts +103 -2
- package/dist/io.d.ts.map +1 -1
- package/dist/io.js +37 -1
- package/dist/io.js.map +1 -1
- package/dist/rendering/components.d.ts +29 -0
- package/dist/rendering/components.d.ts.map +1 -0
- package/dist/rendering/components.js +773 -0
- package/dist/rendering/components.js.map +1 -0
- package/dist/rendering/field-analyzer.d.ts +48 -0
- package/dist/rendering/field-analyzer.d.ts.map +1 -0
- package/dist/rendering/field-analyzer.js +270 -0
- package/dist/rendering/field-analyzer.js.map +1 -0
- package/dist/rendering/field-renderers.d.ts +64 -0
- package/dist/rendering/field-renderers.d.ts.map +1 -0
- package/dist/rendering/field-renderers.js +317 -0
- package/dist/rendering/field-renderers.js.map +1 -0
- package/dist/rendering/index.d.ts +28 -0
- package/dist/rendering/index.d.ts.map +1 -0
- package/dist/rendering/index.js +60 -0
- package/dist/rendering/index.js.map +1 -0
- package/dist/rendering/layout-selector.d.ts +48 -0
- package/dist/rendering/layout-selector.d.ts.map +1 -0
- package/dist/rendering/layout-selector.js +347 -0
- package/dist/rendering/layout-selector.js.map +1 -0
- package/dist/rendering/template-engine.d.ts +41 -0
- package/dist/rendering/template-engine.d.ts.map +1 -0
- package/dist/rendering/template-engine.js +236 -0
- package/dist/rendering/template-engine.js.map +1 -0
- package/dist/schema-extractor.d.ts +30 -0
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +205 -12
- package/dist/schema-extractor.js.map +1 -1
- package/dist/stateful.js +1 -1
- package/dist/stateful.js.map +1 -1
- package/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/ucp/ap2/handlers.d.ts +242 -0
- package/dist/ucp/ap2/handlers.d.ts.map +1 -0
- package/dist/ucp/ap2/handlers.js +482 -0
- package/dist/ucp/ap2/handlers.js.map +1 -0
- package/dist/ucp/ap2/mandates.d.ts +95 -0
- package/dist/ucp/ap2/mandates.d.ts.map +1 -0
- package/dist/ucp/ap2/mandates.js +234 -0
- package/dist/ucp/ap2/mandates.js.map +1 -0
- package/dist/ucp/ap2/types.d.ts +305 -0
- package/dist/ucp/ap2/types.d.ts.map +1 -0
- package/dist/ucp/ap2/types.js +8 -0
- package/dist/ucp/ap2/types.js.map +1 -0
- package/dist/ucp/capabilities/checkout.d.ts +118 -0
- package/dist/ucp/capabilities/checkout.d.ts.map +1 -0
- package/dist/ucp/capabilities/checkout.js +344 -0
- package/dist/ucp/capabilities/checkout.js.map +1 -0
- package/dist/ucp/capabilities/identity.d.ts +130 -0
- package/dist/ucp/capabilities/identity.d.ts.map +1 -0
- package/dist/ucp/capabilities/identity.js +290 -0
- package/dist/ucp/capabilities/identity.js.map +1 -0
- package/dist/ucp/capabilities/order.d.ts +142 -0
- package/dist/ucp/capabilities/order.d.ts.map +1 -0
- package/dist/ucp/capabilities/order.js +383 -0
- package/dist/ucp/capabilities/order.js.map +1 -0
- package/dist/ucp/index.d.ts +18 -0
- package/dist/ucp/index.d.ts.map +1 -0
- package/dist/ucp/index.js +19 -0
- package/dist/ucp/index.js.map +1 -0
- package/dist/ucp/manifest.d.ts +62 -0
- package/dist/ucp/manifest.d.ts.map +1 -0
- package/dist/ucp/manifest.js +180 -0
- package/dist/ucp/manifest.js.map +1 -0
- package/dist/ucp/types.d.ts +327 -0
- package/dist/ucp/types.d.ts.map +1 -0
- package/dist/ucp/types.js +8 -0
- package/dist/ucp/types.js.map +1 -0
- package/package.json +3 -4
- package/src/auto-ui.ts +1 -1
- package/src/base.ts +2 -2
- package/src/cli-ui-renderer.ts +1 -1
- package/src/design-system/index.ts +30 -0
- package/src/design-system/tokens.ts +451 -0
- package/src/design-system/transaction-ui.ts +1038 -0
- package/src/generator.ts +58 -2
- package/src/index.ts +135 -124
- package/src/io.ts +108 -3
- package/src/rendering/components.ts +785 -0
- package/src/rendering/field-analyzer.ts +299 -0
- package/src/rendering/field-renderers.ts +356 -0
- package/src/rendering/index.ts +63 -0
- package/src/rendering/layout-selector.ts +390 -0
- package/src/rendering/template-engine.ts +254 -0
- package/src/schema-extractor.ts +225 -12
- package/src/stateful.ts +1 -1
- package/src/types.ts +10 -1
- package/src/ucp/ap2/handlers.ts +779 -0
- package/src/ucp/ap2/mandates.ts +354 -0
- package/src/ucp/ap2/types.ts +441 -0
- package/src/ucp/capabilities/checkout.ts +497 -0
- package/src/ucp/capabilities/identity.ts +425 -0
- package/src/ucp/capabilities/order.ts +549 -0
- package/src/ucp/index.ts +27 -0
- package/src/ucp/manifest.ts +257 -0
- package/src/ucp/types.ts +454 -0
- package/dist/cli-formatter.d.ts +0 -92
- package/dist/cli-formatter.d.ts.map +0 -1
- package/dist/cli-formatter.js +0 -486
- package/dist/cli-formatter.js.map +0 -1
- package/dist/context.d.ts +0 -6
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -3
- package/dist/context.js.map +0 -1
- package/dist/elicit.d.ts +0 -93
- package/dist/elicit.d.ts.map +0 -1
- package/dist/elicit.js +0 -373
- package/dist/elicit.js.map +0 -1
- package/dist/mcp-client.d.ts +0 -218
- package/dist/mcp-client.d.ts.map +0 -1
- package/dist/mcp-client.js +0 -424
- package/dist/mcp-client.js.map +0 -1
- package/dist/mcp-sdk-transport.d.ts +0 -88
- package/dist/mcp-sdk-transport.d.ts.map +0 -1
- package/dist/mcp-sdk-transport.js +0 -360
- package/dist/mcp-sdk-transport.js.map +0 -1
- package/dist/photon-config.d.ts +0 -86
- package/dist/photon-config.d.ts.map +0 -1
- package/dist/photon-config.js +0 -156
- package/dist/photon-config.js.map +0 -1
- package/dist/progress.d.ts +0 -93
- package/dist/progress.d.ts.map +0 -1
- package/dist/progress.js +0 -195
- package/dist/progress.js.map +0 -1
- package/src/cli-formatter.ts +0 -579
- package/src/context.ts +0 -7
- package/src/elicit.ts +0 -438
- package/src/mcp-client.ts +0 -561
- package/src/mcp-sdk-transport.ts +0 -449
- package/src/photon-config.ts +0 -201
- package/src/progress.ts +0 -224
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UCP Order Management Capability Implementation
|
|
3
|
+
*
|
|
4
|
+
* Provides order tracking, returns, and webhook handling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as crypto from 'crypto';
|
|
8
|
+
import {
|
|
9
|
+
Order,
|
|
10
|
+
OrderStatus,
|
|
11
|
+
Shipment,
|
|
12
|
+
ShipmentStatus,
|
|
13
|
+
TrackingEvent,
|
|
14
|
+
ReturnRequest,
|
|
15
|
+
Adjustment,
|
|
16
|
+
WebhookRegistration,
|
|
17
|
+
WebhookEventType,
|
|
18
|
+
WebhookPayload,
|
|
19
|
+
Money
|
|
20
|
+
} from '../types.js';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Order Storage Interface
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
export interface OrderStorage {
|
|
27
|
+
get(orderId: string): Promise<Order | null>;
|
|
28
|
+
set(order: Order): Promise<void>;
|
|
29
|
+
list(params?: {
|
|
30
|
+
merchantId?: string;
|
|
31
|
+
status?: OrderStatus[];
|
|
32
|
+
limit?: number;
|
|
33
|
+
offset?: number;
|
|
34
|
+
}): Promise<{ orders: Order[]; total: number }>;
|
|
35
|
+
delete(orderId: string): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* In-memory order storage (for development/testing)
|
|
40
|
+
*/
|
|
41
|
+
export class MemoryOrderStorage implements OrderStorage {
|
|
42
|
+
private orders = new Map<string, Order>();
|
|
43
|
+
|
|
44
|
+
async get(orderId: string): Promise<Order | null> {
|
|
45
|
+
return this.orders.get(orderId) || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async set(order: Order): Promise<void> {
|
|
49
|
+
this.orders.set(order.id, order);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async list(params?: {
|
|
53
|
+
merchantId?: string;
|
|
54
|
+
status?: OrderStatus[];
|
|
55
|
+
limit?: number;
|
|
56
|
+
offset?: number;
|
|
57
|
+
}): Promise<{ orders: Order[]; total: number }> {
|
|
58
|
+
let orders = Array.from(this.orders.values());
|
|
59
|
+
|
|
60
|
+
if (params?.merchantId) {
|
|
61
|
+
orders = orders.filter(o => o.merchantId === params.merchantId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (params?.status?.length) {
|
|
65
|
+
orders = orders.filter(o => params.status!.includes(o.status));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Sort by creation date descending
|
|
69
|
+
orders.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
70
|
+
|
|
71
|
+
const total = orders.length;
|
|
72
|
+
const offset = params?.offset || 0;
|
|
73
|
+
const limit = params?.limit || 50;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
orders: orders.slice(offset, offset + limit),
|
|
77
|
+
total
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async delete(orderId: string): Promise<void> {
|
|
82
|
+
this.orders.delete(orderId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Webhook Dispatcher
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
export interface WebhookDispatcher {
|
|
91
|
+
dispatch(webhook: WebhookRegistration, payload: WebhookPayload): Promise<boolean>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Default webhook dispatcher using fetch
|
|
96
|
+
*/
|
|
97
|
+
export class HttpWebhookDispatcher implements WebhookDispatcher {
|
|
98
|
+
async dispatch(webhook: WebhookRegistration, payload: WebhookPayload): Promise<boolean> {
|
|
99
|
+
try {
|
|
100
|
+
// Generate signature
|
|
101
|
+
const signature = this.generateSignature(payload, webhook.secret);
|
|
102
|
+
|
|
103
|
+
const response = await fetch(webhook.url, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
'X-UCP-Signature': signature,
|
|
108
|
+
'X-UCP-Event': payload.type
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify(payload)
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return response.ok;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(`Webhook dispatch failed: ${error}`);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private generateSignature(payload: WebhookPayload, secret: string): string {
|
|
121
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
122
|
+
hmac.update(JSON.stringify(payload));
|
|
123
|
+
return `sha256=${hmac.digest('hex')}`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Order Service
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
export interface OrderServiceConfig {
|
|
132
|
+
merchantId: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class OrderService {
|
|
136
|
+
private storage: OrderStorage;
|
|
137
|
+
private webhooks: WebhookRegistration[] = [];
|
|
138
|
+
private dispatcher: WebhookDispatcher;
|
|
139
|
+
private config: OrderServiceConfig;
|
|
140
|
+
|
|
141
|
+
constructor(
|
|
142
|
+
config: OrderServiceConfig,
|
|
143
|
+
storage?: OrderStorage,
|
|
144
|
+
dispatcher?: WebhookDispatcher
|
|
145
|
+
) {
|
|
146
|
+
this.config = config;
|
|
147
|
+
this.storage = storage || new MemoryOrderStorage();
|
|
148
|
+
this.dispatcher = dispatcher || new HttpWebhookDispatcher();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --------------------------------------------------------------------------
|
|
152
|
+
// Order Retrieval
|
|
153
|
+
// --------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get order by ID
|
|
157
|
+
*/
|
|
158
|
+
async getOrder(orderId: string): Promise<Order> {
|
|
159
|
+
const order = await this.storage.get(orderId);
|
|
160
|
+
if (!order) {
|
|
161
|
+
throw new Error(`Order not found: ${orderId}`);
|
|
162
|
+
}
|
|
163
|
+
return order;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* List orders with optional filters
|
|
168
|
+
*/
|
|
169
|
+
async listOrders(params?: {
|
|
170
|
+
status?: OrderStatus[];
|
|
171
|
+
limit?: number;
|
|
172
|
+
cursor?: string;
|
|
173
|
+
}): Promise<{ orders: Order[]; nextCursor?: string }> {
|
|
174
|
+
const offset = params?.cursor ? parseInt(params.cursor, 10) : 0;
|
|
175
|
+
const limit = params?.limit || 20;
|
|
176
|
+
|
|
177
|
+
const result = await this.storage.list({
|
|
178
|
+
merchantId: this.config.merchantId,
|
|
179
|
+
status: params?.status,
|
|
180
|
+
limit: limit + 1, // Fetch one extra to check for more
|
|
181
|
+
offset
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const hasMore = result.orders.length > limit;
|
|
185
|
+
const orders = hasMore ? result.orders.slice(0, limit) : result.orders;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
orders,
|
|
189
|
+
nextCursor: hasMore ? String(offset + limit) : undefined
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// --------------------------------------------------------------------------
|
|
194
|
+
// Order Updates
|
|
195
|
+
// --------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Update order status
|
|
199
|
+
*/
|
|
200
|
+
async updateStatus(orderId: string, status: OrderStatus, reason?: string): Promise<Order> {
|
|
201
|
+
const order = await this.getOrder(orderId);
|
|
202
|
+
const previousStatus = order.status;
|
|
203
|
+
|
|
204
|
+
order.status = status;
|
|
205
|
+
order.updatedAt = new Date().toISOString();
|
|
206
|
+
|
|
207
|
+
if (reason && status === 'cancelled') {
|
|
208
|
+
order.adjustments.push({
|
|
209
|
+
id: `adj_${crypto.randomUUID()}`,
|
|
210
|
+
type: 'refund',
|
|
211
|
+
amount: order.totals.total,
|
|
212
|
+
reason,
|
|
213
|
+
createdAt: new Date().toISOString()
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
await this.storage.set(order);
|
|
218
|
+
|
|
219
|
+
// Dispatch webhook
|
|
220
|
+
await this.dispatchWebhook('order.updated', {
|
|
221
|
+
order,
|
|
222
|
+
previousStatus,
|
|
223
|
+
newStatus: status
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return order;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Cancel order
|
|
231
|
+
*/
|
|
232
|
+
async cancelOrder(orderId: string, reason?: string): Promise<Order> {
|
|
233
|
+
const order = await this.getOrder(orderId);
|
|
234
|
+
|
|
235
|
+
if (!['pending', 'confirmed', 'processing'].includes(order.status)) {
|
|
236
|
+
throw new Error(`Cannot cancel order in ${order.status} status`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return this.updateStatus(orderId, 'cancelled', reason || 'Cancelled by customer');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// --------------------------------------------------------------------------
|
|
243
|
+
// Shipment Tracking
|
|
244
|
+
// --------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Add shipment to order
|
|
248
|
+
*/
|
|
249
|
+
async addShipment(orderId: string, shipment: Omit<Shipment, 'id' | 'events'>): Promise<Order> {
|
|
250
|
+
const order = await this.getOrder(orderId);
|
|
251
|
+
|
|
252
|
+
const newShipment: Shipment = {
|
|
253
|
+
...shipment,
|
|
254
|
+
id: `ship_${crypto.randomUUID()}`,
|
|
255
|
+
events: [{
|
|
256
|
+
timestamp: new Date().toISOString(),
|
|
257
|
+
status: 'label_created',
|
|
258
|
+
description: 'Shipping label created'
|
|
259
|
+
}]
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
order.shipments.push(newShipment);
|
|
263
|
+
order.status = 'shipped';
|
|
264
|
+
order.updatedAt = new Date().toISOString();
|
|
265
|
+
|
|
266
|
+
await this.storage.set(order);
|
|
267
|
+
|
|
268
|
+
// Dispatch webhook
|
|
269
|
+
await this.dispatchWebhook('shipment.created', {
|
|
270
|
+
order,
|
|
271
|
+
shipment: newShipment
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return order;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Update shipment status
|
|
279
|
+
*/
|
|
280
|
+
async updateShipment(
|
|
281
|
+
orderId: string,
|
|
282
|
+
shipmentId: string,
|
|
283
|
+
update: {
|
|
284
|
+
status: ShipmentStatus;
|
|
285
|
+
location?: string;
|
|
286
|
+
description: string;
|
|
287
|
+
}
|
|
288
|
+
): Promise<Order> {
|
|
289
|
+
const order = await this.getOrder(orderId);
|
|
290
|
+
const shipment = order.shipments.find(s => s.id === shipmentId);
|
|
291
|
+
|
|
292
|
+
if (!shipment) {
|
|
293
|
+
throw new Error(`Shipment not found: ${shipmentId}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
shipment.status = update.status;
|
|
297
|
+
shipment.events.push({
|
|
298
|
+
timestamp: new Date().toISOString(),
|
|
299
|
+
status: update.status,
|
|
300
|
+
location: update.location,
|
|
301
|
+
description: update.description
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Update order status if delivered
|
|
305
|
+
if (update.status === 'delivered') {
|
|
306
|
+
const allDelivered = order.shipments.every(s => s.status === 'delivered');
|
|
307
|
+
if (allDelivered) {
|
|
308
|
+
order.status = 'delivered';
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
order.updatedAt = new Date().toISOString();
|
|
313
|
+
await this.storage.set(order);
|
|
314
|
+
|
|
315
|
+
// Dispatch webhook
|
|
316
|
+
const eventType = update.status === 'delivered' ? 'delivery.completed' : 'shipment.updated';
|
|
317
|
+
await this.dispatchWebhook(eventType, {
|
|
318
|
+
order,
|
|
319
|
+
shipment
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return order;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get shipment tracking info
|
|
327
|
+
*/
|
|
328
|
+
async trackShipment(orderId: string, shipmentId?: string): Promise<Shipment[]> {
|
|
329
|
+
const order = await this.getOrder(orderId);
|
|
330
|
+
|
|
331
|
+
if (shipmentId) {
|
|
332
|
+
const shipment = order.shipments.find(s => s.id === shipmentId);
|
|
333
|
+
if (!shipment) {
|
|
334
|
+
throw new Error(`Shipment not found: ${shipmentId}`);
|
|
335
|
+
}
|
|
336
|
+
return [shipment];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return order.shipments;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// --------------------------------------------------------------------------
|
|
343
|
+
// Returns & Refunds
|
|
344
|
+
// --------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Initiate return request
|
|
348
|
+
*/
|
|
349
|
+
async initiateReturn(orderId: string, params: {
|
|
350
|
+
items: { lineItemId: string; quantity: number; reason: string }[];
|
|
351
|
+
}): Promise<ReturnRequest> {
|
|
352
|
+
const order = await this.getOrder(orderId);
|
|
353
|
+
|
|
354
|
+
if (order.status !== 'delivered') {
|
|
355
|
+
throw new Error('Can only return delivered orders');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Validate items
|
|
359
|
+
for (const item of params.items) {
|
|
360
|
+
const lineItem = order.lineItems.find(li => li.id === item.lineItemId);
|
|
361
|
+
if (!lineItem) {
|
|
362
|
+
throw new Error(`Line item not found: ${item.lineItemId}`);
|
|
363
|
+
}
|
|
364
|
+
if (item.quantity > lineItem.quantity) {
|
|
365
|
+
throw new Error(`Return quantity exceeds order quantity for ${lineItem.label}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Calculate refund amount
|
|
370
|
+
const refundAmount: Money = {
|
|
371
|
+
amount: params.items.reduce((sum, item) => {
|
|
372
|
+
const lineItem = order.lineItems.find(li => li.id === item.lineItemId)!;
|
|
373
|
+
return sum + (lineItem.unitPrice.amount * item.quantity);
|
|
374
|
+
}, 0),
|
|
375
|
+
currency: order.totals.total.currency
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const returnRequest: ReturnRequest = {
|
|
379
|
+
id: `ret_${crypto.randomUUID()}`,
|
|
380
|
+
status: 'requested',
|
|
381
|
+
items: params.items,
|
|
382
|
+
createdAt: new Date().toISOString(),
|
|
383
|
+
updatedAt: new Date().toISOString(),
|
|
384
|
+
refundAmount
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
order.returns.push(returnRequest);
|
|
388
|
+
order.updatedAt = new Date().toISOString();
|
|
389
|
+
|
|
390
|
+
await this.storage.set(order);
|
|
391
|
+
|
|
392
|
+
// Dispatch webhook
|
|
393
|
+
await this.dispatchWebhook('return.requested', {
|
|
394
|
+
order,
|
|
395
|
+
return: returnRequest
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
return returnRequest;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Approve return request
|
|
403
|
+
*/
|
|
404
|
+
async approveReturn(orderId: string, returnId: string, params?: {
|
|
405
|
+
returnLabel?: {
|
|
406
|
+
carrier: string;
|
|
407
|
+
trackingNumber: string;
|
|
408
|
+
labelUrl: string;
|
|
409
|
+
};
|
|
410
|
+
}): Promise<ReturnRequest> {
|
|
411
|
+
const order = await this.getOrder(orderId);
|
|
412
|
+
const returnRequest = order.returns.find(r => r.id === returnId);
|
|
413
|
+
|
|
414
|
+
if (!returnRequest) {
|
|
415
|
+
throw new Error(`Return request not found: ${returnId}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (returnRequest.status !== 'requested') {
|
|
419
|
+
throw new Error(`Cannot approve return in ${returnRequest.status} status`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
returnRequest.status = 'approved';
|
|
423
|
+
returnRequest.updatedAt = new Date().toISOString();
|
|
424
|
+
returnRequest.returnLabel = params?.returnLabel;
|
|
425
|
+
|
|
426
|
+
order.updatedAt = new Date().toISOString();
|
|
427
|
+
await this.storage.set(order);
|
|
428
|
+
|
|
429
|
+
// Dispatch webhook
|
|
430
|
+
await this.dispatchWebhook('return.approved', {
|
|
431
|
+
order,
|
|
432
|
+
return: returnRequest
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
return returnRequest;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Process refund for return
|
|
440
|
+
*/
|
|
441
|
+
async processRefund(orderId: string, returnId: string): Promise<Adjustment> {
|
|
442
|
+
const order = await this.getOrder(orderId);
|
|
443
|
+
const returnRequest = order.returns.find(r => r.id === returnId);
|
|
444
|
+
|
|
445
|
+
if (!returnRequest) {
|
|
446
|
+
throw new Error(`Return request not found: ${returnId}`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (returnRequest.status !== 'received' && returnRequest.status !== 'approved') {
|
|
450
|
+
throw new Error(`Cannot process refund for return in ${returnRequest.status} status`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const adjustment: Adjustment = {
|
|
454
|
+
id: `adj_${crypto.randomUUID()}`,
|
|
455
|
+
type: 'refund',
|
|
456
|
+
amount: returnRequest.refundAmount!,
|
|
457
|
+
reason: `Return refund for ${returnRequest.items.length} item(s)`,
|
|
458
|
+
createdAt: new Date().toISOString(),
|
|
459
|
+
lineItemIds: returnRequest.items.map(i => i.lineItemId)
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
returnRequest.status = 'refunded';
|
|
463
|
+
returnRequest.updatedAt = new Date().toISOString();
|
|
464
|
+
|
|
465
|
+
order.adjustments.push(adjustment);
|
|
466
|
+
order.updatedAt = new Date().toISOString();
|
|
467
|
+
|
|
468
|
+
// Check if fully refunded
|
|
469
|
+
const totalRefunded = order.adjustments
|
|
470
|
+
.filter(a => a.type === 'refund')
|
|
471
|
+
.reduce((sum, a) => sum + a.amount.amount, 0);
|
|
472
|
+
|
|
473
|
+
if (totalRefunded >= order.totals.total.amount) {
|
|
474
|
+
order.status = 'refunded';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
await this.storage.set(order);
|
|
478
|
+
|
|
479
|
+
// Dispatch webhook
|
|
480
|
+
await this.dispatchWebhook('refund.created', {
|
|
481
|
+
order,
|
|
482
|
+
adjustment
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
return adjustment;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// --------------------------------------------------------------------------
|
|
489
|
+
// Webhooks
|
|
490
|
+
// --------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Register webhook
|
|
494
|
+
*/
|
|
495
|
+
async registerWebhook(params: {
|
|
496
|
+
url: string;
|
|
497
|
+
events: WebhookEventType[];
|
|
498
|
+
secret: string;
|
|
499
|
+
}): Promise<WebhookRegistration> {
|
|
500
|
+
const webhook: WebhookRegistration = {
|
|
501
|
+
id: `wh_${crypto.randomUUID()}`,
|
|
502
|
+
url: params.url,
|
|
503
|
+
events: params.events,
|
|
504
|
+
secret: params.secret,
|
|
505
|
+
active: true,
|
|
506
|
+
createdAt: new Date().toISOString()
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
this.webhooks.push(webhook);
|
|
510
|
+
return webhook;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* List webhooks
|
|
515
|
+
*/
|
|
516
|
+
async listWebhooks(): Promise<WebhookRegistration[]> {
|
|
517
|
+
return this.webhooks.filter(w => w.active);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Delete webhook
|
|
522
|
+
*/
|
|
523
|
+
async deleteWebhook(webhookId: string): Promise<void> {
|
|
524
|
+
const index = this.webhooks.findIndex(w => w.id === webhookId);
|
|
525
|
+
if (index !== -1) {
|
|
526
|
+
this.webhooks[index].active = false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Dispatch webhook to registered endpoints
|
|
532
|
+
*/
|
|
533
|
+
private async dispatchWebhook(type: WebhookEventType, data: any): Promise<void> {
|
|
534
|
+
const payload: WebhookPayload = {
|
|
535
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
536
|
+
type,
|
|
537
|
+
timestamp: new Date().toISOString(),
|
|
538
|
+
data
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const relevantWebhooks = this.webhooks.filter(
|
|
542
|
+
w => w.active && w.events.includes(type)
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
await Promise.all(
|
|
546
|
+
relevantWebhooks.map(w => this.dispatcher.dispatch(w, payload))
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
package/src/ucp/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Commerce Protocol (UCP) Module
|
|
3
|
+
*
|
|
4
|
+
* Provides UCP support for Photon, enabling agentic commerce
|
|
5
|
+
* with checkout, identity, order management, and AP2 payments.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Core UCP Types
|
|
9
|
+
export * from './types.js';
|
|
10
|
+
|
|
11
|
+
// AP2 Payment Types & Helpers
|
|
12
|
+
export * from './ap2/types.js';
|
|
13
|
+
export * from './ap2/mandates.js';
|
|
14
|
+
export * from './ap2/handlers.js';
|
|
15
|
+
|
|
16
|
+
// Manifest Generation
|
|
17
|
+
export * from './manifest.js';
|
|
18
|
+
|
|
19
|
+
// Capabilities
|
|
20
|
+
export { CheckoutService, MemorySessionStorage } from './capabilities/checkout.js';
|
|
21
|
+
export type { SessionStorage, CheckoutConfig, TaxCalculator, DiscountValidator, FulfillmentProvider } from './capabilities/checkout.js';
|
|
22
|
+
|
|
23
|
+
export { OrderService, MemoryOrderStorage, HttpWebhookDispatcher } from './capabilities/order.js';
|
|
24
|
+
export type { OrderStorage, WebhookDispatcher, OrderServiceConfig } from './capabilities/order.js';
|
|
25
|
+
|
|
26
|
+
export { IdentityService, MemoryTokenStorage } from './capabilities/identity.js';
|
|
27
|
+
export type { TokenStorage, IdentityServiceConfig } from './capabilities/identity.js';
|