@portel/photon-core 1.4.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/README.md +123 -0
- package/dist/auto-ui.d.ts +103 -0
- package/dist/auto-ui.d.ts.map +1 -0
- package/dist/auto-ui.js +275 -0
- package/dist/auto-ui.js.map +1 -0
- package/dist/base.d.ts +9 -2
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +23 -10
- package/dist/base.js.map +1 -1
- package/dist/cli-ui-renderer.d.ts +31 -0
- package/dist/cli-ui-renderer.d.ts.map +1 -0
- package/dist/cli-ui-renderer.js +224 -0
- package/dist/cli-ui-renderer.js.map +1 -0
- package/dist/dependency-manager.d.ts.map +1 -1
- package/dist/dependency-manager.js +0 -1
- package/dist/dependency-manager.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 +58 -8
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +9 -4
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -44
- package/dist/index.js.map +1 -1
- package/dist/io.d.ts +395 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +304 -0
- package/dist/io.js.map +1 -0
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +2 -1
- package/dist/path-resolver.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.d.ts +63 -0
- package/dist/stateful.d.ts.map +1 -1
- package/dist/stateful.js +222 -0
- 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 +413 -0
- package/src/base.ts +22 -9
- package/src/cli-ui-renderer.ts +264 -0
- package/src/dependency-manager.ts +0 -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 +68 -8
- package/src/index.ts +159 -101
- package/src/io.ts +493 -0
- package/src/path-resolver.ts +2 -1
- 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 +301 -0
- 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/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/src/cli-formatter.ts +0 -579
- 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
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UCP Checkout Capability Implementation
|
|
3
|
+
*
|
|
4
|
+
* Provides cart management, pricing, and checkout session handling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as crypto from 'crypto';
|
|
8
|
+
import {
|
|
9
|
+
CheckoutSession,
|
|
10
|
+
CheckoutSessionStatus,
|
|
11
|
+
LineItem,
|
|
12
|
+
DiscountAllocation,
|
|
13
|
+
Totals,
|
|
14
|
+
FulfillmentOption,
|
|
15
|
+
FulfillmentDestination,
|
|
16
|
+
BuyerInfo,
|
|
17
|
+
PaymentMethodInfo,
|
|
18
|
+
Order,
|
|
19
|
+
Money
|
|
20
|
+
} from '../types.js';
|
|
21
|
+
import { PaymentMandate, CartMandate } from '../ap2/types.js';
|
|
22
|
+
import { createCartMandate, signCartMandate, createPaymentMandate } from '../ap2/mandates.js';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Session Storage Interface
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export interface SessionStorage {
|
|
29
|
+
get(sessionId: string): Promise<CheckoutSession | null>;
|
|
30
|
+
set(session: CheckoutSession): Promise<void>;
|
|
31
|
+
delete(sessionId: string): Promise<void>;
|
|
32
|
+
cleanup(): Promise<void>; // Remove expired sessions
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* In-memory session storage (for development/testing)
|
|
37
|
+
*/
|
|
38
|
+
export class MemorySessionStorage implements SessionStorage {
|
|
39
|
+
private sessions = new Map<string, CheckoutSession>();
|
|
40
|
+
|
|
41
|
+
async get(sessionId: string): Promise<CheckoutSession | null> {
|
|
42
|
+
const session = this.sessions.get(sessionId);
|
|
43
|
+
if (!session) return null;
|
|
44
|
+
|
|
45
|
+
// Check expiration
|
|
46
|
+
if (new Date(session.expiresAt) < new Date()) {
|
|
47
|
+
this.sessions.delete(sessionId);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return session;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async set(session: CheckoutSession): Promise<void> {
|
|
55
|
+
this.sessions.set(session.id, session);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async delete(sessionId: string): Promise<void> {
|
|
59
|
+
this.sessions.delete(sessionId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async cleanup(): Promise<void> {
|
|
63
|
+
const now = new Date();
|
|
64
|
+
for (const [id, session] of this.sessions) {
|
|
65
|
+
if (new Date(session.expiresAt) < now) {
|
|
66
|
+
this.sessions.delete(id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Checkout Service
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
export interface CheckoutConfig {
|
|
77
|
+
merchantId: string;
|
|
78
|
+
merchantName: string;
|
|
79
|
+
defaultCurrency: string;
|
|
80
|
+
sessionTTLMinutes: number;
|
|
81
|
+
taxCalculator?: TaxCalculator;
|
|
82
|
+
discountValidator?: DiscountValidator;
|
|
83
|
+
fulfillmentProvider?: FulfillmentProvider;
|
|
84
|
+
paymentMethods: PaymentMethodInfo[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface TaxCalculator {
|
|
88
|
+
calculate(items: LineItem[], destination?: FulfillmentDestination): Promise<{
|
|
89
|
+
taxAmount: Money;
|
|
90
|
+
taxLines: { label: string; rate: number; amount: Money }[];
|
|
91
|
+
}>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface DiscountValidator {
|
|
95
|
+
validate(code: string, session: CheckoutSession): Promise<{
|
|
96
|
+
valid: boolean;
|
|
97
|
+
discount?: DiscountAllocation;
|
|
98
|
+
error?: string;
|
|
99
|
+
}>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface FulfillmentProvider {
|
|
103
|
+
getOptions(items: LineItem[], destination?: FulfillmentDestination): Promise<FulfillmentOption[]>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class CheckoutService {
|
|
107
|
+
private storage: SessionStorage;
|
|
108
|
+
private config: CheckoutConfig;
|
|
109
|
+
|
|
110
|
+
constructor(config: CheckoutConfig, storage?: SessionStorage) {
|
|
111
|
+
this.config = config;
|
|
112
|
+
this.storage = storage || new MemorySessionStorage();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --------------------------------------------------------------------------
|
|
116
|
+
// Session Management
|
|
117
|
+
// --------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create a new checkout session
|
|
121
|
+
*/
|
|
122
|
+
async createSession(params?: {
|
|
123
|
+
currency?: string;
|
|
124
|
+
locale?: string;
|
|
125
|
+
metadata?: Record<string, any>;
|
|
126
|
+
}): Promise<CheckoutSession> {
|
|
127
|
+
const now = new Date();
|
|
128
|
+
const expiresAt = new Date(now.getTime() + this.config.sessionTTLMinutes * 60 * 1000);
|
|
129
|
+
|
|
130
|
+
const session: CheckoutSession = {
|
|
131
|
+
id: `cs_${crypto.randomUUID()}`,
|
|
132
|
+
merchantId: this.config.merchantId,
|
|
133
|
+
status: 'open',
|
|
134
|
+
createdAt: now.toISOString(),
|
|
135
|
+
updatedAt: now.toISOString(),
|
|
136
|
+
expiresAt: expiresAt.toISOString(),
|
|
137
|
+
lineItems: [],
|
|
138
|
+
discounts: [],
|
|
139
|
+
fulfillmentOptions: [],
|
|
140
|
+
availablePaymentMethods: this.config.paymentMethods,
|
|
141
|
+
totals: this.calculateTotals([]),
|
|
142
|
+
metadata: params?.metadata
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
await this.storage.set(session);
|
|
146
|
+
return session;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get checkout session by ID
|
|
151
|
+
*/
|
|
152
|
+
async getSession(sessionId: string): Promise<CheckoutSession> {
|
|
153
|
+
const session = await this.storage.get(sessionId);
|
|
154
|
+
if (!session) {
|
|
155
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
156
|
+
}
|
|
157
|
+
return session;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --------------------------------------------------------------------------
|
|
161
|
+
// Cart Management
|
|
162
|
+
// --------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Update cart items
|
|
166
|
+
*/
|
|
167
|
+
async updateCart(sessionId: string, updates: {
|
|
168
|
+
add?: Omit<LineItem, 'id' | 'totalPrice'>[];
|
|
169
|
+
remove?: string[];
|
|
170
|
+
update?: { id: string; quantity: number }[];
|
|
171
|
+
}): Promise<CheckoutSession> {
|
|
172
|
+
const session = await this.getSession(sessionId);
|
|
173
|
+
this.assertSessionOpen(session);
|
|
174
|
+
|
|
175
|
+
let items = [...session.lineItems];
|
|
176
|
+
|
|
177
|
+
// Remove items
|
|
178
|
+
if (updates.remove) {
|
|
179
|
+
items = items.filter(item => !updates.remove!.includes(item.id));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Update quantities
|
|
183
|
+
if (updates.update) {
|
|
184
|
+
for (const update of updates.update) {
|
|
185
|
+
const item = items.find(i => i.id === update.id);
|
|
186
|
+
if (item) {
|
|
187
|
+
if (update.quantity <= 0) {
|
|
188
|
+
items = items.filter(i => i.id !== update.id);
|
|
189
|
+
} else {
|
|
190
|
+
item.quantity = update.quantity;
|
|
191
|
+
item.totalPrice = {
|
|
192
|
+
amount: item.unitPrice.amount * update.quantity,
|
|
193
|
+
currency: item.unitPrice.currency
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Add new items
|
|
201
|
+
if (updates.add) {
|
|
202
|
+
for (const newItem of updates.add) {
|
|
203
|
+
items.push({
|
|
204
|
+
...newItem,
|
|
205
|
+
id: `li_${crypto.randomUUID()}`,
|
|
206
|
+
totalPrice: {
|
|
207
|
+
amount: newItem.unitPrice.amount * newItem.quantity,
|
|
208
|
+
currency: newItem.unitPrice.currency
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
session.lineItems = items;
|
|
215
|
+
session.totals = this.calculateTotals(items, session.discounts, session.selectedFulfillment);
|
|
216
|
+
session.updatedAt = new Date().toISOString();
|
|
217
|
+
|
|
218
|
+
// Update fulfillment options if provider available
|
|
219
|
+
if (this.config.fulfillmentProvider) {
|
|
220
|
+
session.fulfillmentOptions = await this.config.fulfillmentProvider.getOptions(
|
|
221
|
+
items,
|
|
222
|
+
session.selectedFulfillment?.destination
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await this.storage.set(session);
|
|
227
|
+
return session;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Set buyer information
|
|
232
|
+
*/
|
|
233
|
+
async setBuyer(sessionId: string, buyer: BuyerInfo): Promise<CheckoutSession> {
|
|
234
|
+
const session = await this.getSession(sessionId);
|
|
235
|
+
this.assertSessionOpen(session);
|
|
236
|
+
|
|
237
|
+
session.buyer = buyer;
|
|
238
|
+
session.updatedAt = new Date().toISOString();
|
|
239
|
+
|
|
240
|
+
await this.storage.set(session);
|
|
241
|
+
return session;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Set fulfillment option
|
|
246
|
+
*/
|
|
247
|
+
async setFulfillment(sessionId: string, params: {
|
|
248
|
+
optionId: string;
|
|
249
|
+
destination: FulfillmentDestination;
|
|
250
|
+
}): Promise<CheckoutSession> {
|
|
251
|
+
const session = await this.getSession(sessionId);
|
|
252
|
+
this.assertSessionOpen(session);
|
|
253
|
+
|
|
254
|
+
const option = session.fulfillmentOptions.find(o => o.id === params.optionId);
|
|
255
|
+
if (!option) {
|
|
256
|
+
throw new Error(`Fulfillment option not found: ${params.optionId}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
session.selectedFulfillment = {
|
|
260
|
+
optionId: params.optionId,
|
|
261
|
+
destination: params.destination
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Recalculate totals with shipping
|
|
265
|
+
session.totals = this.calculateTotals(
|
|
266
|
+
session.lineItems,
|
|
267
|
+
session.discounts,
|
|
268
|
+
session.selectedFulfillment,
|
|
269
|
+
option.price
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// Recalculate tax if calculator available
|
|
273
|
+
if (this.config.taxCalculator) {
|
|
274
|
+
const taxResult = await this.config.taxCalculator.calculate(
|
|
275
|
+
session.lineItems,
|
|
276
|
+
params.destination
|
|
277
|
+
);
|
|
278
|
+
session.totals.tax = taxResult.taxAmount;
|
|
279
|
+
session.totals.taxLines = taxResult.taxLines;
|
|
280
|
+
session.totals.total = {
|
|
281
|
+
amount: session.totals.subtotal.amount - session.totals.discounts.amount +
|
|
282
|
+
session.totals.shipping.amount + session.totals.tax.amount,
|
|
283
|
+
currency: session.totals.subtotal.currency
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
session.updatedAt = new Date().toISOString();
|
|
288
|
+
await this.storage.set(session);
|
|
289
|
+
return session;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// --------------------------------------------------------------------------
|
|
293
|
+
// Discounts
|
|
294
|
+
// --------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Apply discount code
|
|
298
|
+
*/
|
|
299
|
+
async applyDiscount(sessionId: string, code: string): Promise<{
|
|
300
|
+
success: boolean;
|
|
301
|
+
session: CheckoutSession;
|
|
302
|
+
error?: string;
|
|
303
|
+
}> {
|
|
304
|
+
const session = await this.getSession(sessionId);
|
|
305
|
+
this.assertSessionOpen(session);
|
|
306
|
+
|
|
307
|
+
// Check if already applied
|
|
308
|
+
if (session.discounts.some(d => d.code === code)) {
|
|
309
|
+
return { success: false, session, error: 'Discount already applied' };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Validate discount
|
|
313
|
+
if (this.config.discountValidator) {
|
|
314
|
+
const result = await this.config.discountValidator.validate(code, session);
|
|
315
|
+
if (!result.valid) {
|
|
316
|
+
return { success: false, session, error: result.error };
|
|
317
|
+
}
|
|
318
|
+
if (result.discount) {
|
|
319
|
+
session.discounts.push(result.discount);
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
// Simple default discount handling
|
|
323
|
+
const discount: DiscountAllocation = {
|
|
324
|
+
code,
|
|
325
|
+
label: `Discount: ${code}`,
|
|
326
|
+
type: 'percentage',
|
|
327
|
+
value: 10,
|
|
328
|
+
appliedAmount: {
|
|
329
|
+
amount: session.totals.subtotal.amount * 0.1,
|
|
330
|
+
currency: session.totals.subtotal.currency
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
session.discounts.push(discount);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
session.totals = this.calculateTotals(
|
|
337
|
+
session.lineItems,
|
|
338
|
+
session.discounts,
|
|
339
|
+
session.selectedFulfillment
|
|
340
|
+
);
|
|
341
|
+
session.updatedAt = new Date().toISOString();
|
|
342
|
+
|
|
343
|
+
await this.storage.set(session);
|
|
344
|
+
return { success: true, session };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Remove discount code
|
|
349
|
+
*/
|
|
350
|
+
async removeDiscount(sessionId: string, code: string): Promise<CheckoutSession> {
|
|
351
|
+
const session = await this.getSession(sessionId);
|
|
352
|
+
this.assertSessionOpen(session);
|
|
353
|
+
|
|
354
|
+
session.discounts = session.discounts.filter(d => d.code !== code);
|
|
355
|
+
session.totals = this.calculateTotals(
|
|
356
|
+
session.lineItems,
|
|
357
|
+
session.discounts,
|
|
358
|
+
session.selectedFulfillment
|
|
359
|
+
);
|
|
360
|
+
session.updatedAt = new Date().toISOString();
|
|
361
|
+
|
|
362
|
+
await this.storage.set(session);
|
|
363
|
+
return session;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// --------------------------------------------------------------------------
|
|
367
|
+
// Checkout Completion
|
|
368
|
+
// --------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Complete checkout with payment
|
|
372
|
+
*/
|
|
373
|
+
async complete(sessionId: string, payment: {
|
|
374
|
+
methodId?: string;
|
|
375
|
+
mandate?: PaymentMandate;
|
|
376
|
+
}): Promise<Order> {
|
|
377
|
+
const session = await this.getSession(sessionId);
|
|
378
|
+
this.assertSessionOpen(session);
|
|
379
|
+
|
|
380
|
+
// Validate session is ready
|
|
381
|
+
if (session.lineItems.length === 0) {
|
|
382
|
+
throw new Error('Cart is empty');
|
|
383
|
+
}
|
|
384
|
+
if (!session.buyer?.email) {
|
|
385
|
+
throw new Error('Buyer email is required');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Mark as processing
|
|
389
|
+
session.status = 'processing';
|
|
390
|
+
session.updatedAt = new Date().toISOString();
|
|
391
|
+
await this.storage.set(session);
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
// Create order from session
|
|
395
|
+
const order: Order = {
|
|
396
|
+
id: `order_${crypto.randomUUID()}`,
|
|
397
|
+
checkoutSessionId: session.id,
|
|
398
|
+
merchantId: session.merchantId,
|
|
399
|
+
status: 'confirmed',
|
|
400
|
+
createdAt: new Date().toISOString(),
|
|
401
|
+
updatedAt: new Date().toISOString(),
|
|
402
|
+
buyer: session.buyer,
|
|
403
|
+
lineItems: session.lineItems,
|
|
404
|
+
fulfillmentExpectations: session.selectedFulfillment ? [{
|
|
405
|
+
id: `fe_${crypto.randomUUID()}`,
|
|
406
|
+
type: session.selectedFulfillment.destination.type,
|
|
407
|
+
description: `Shipping to ${session.selectedFulfillment.destination.address?.city || 'destination'}`,
|
|
408
|
+
lineItemIds: session.lineItems.map(i => i.id)
|
|
409
|
+
}] : [],
|
|
410
|
+
shipments: [],
|
|
411
|
+
paymentMethod: session.selectedPaymentMethod || session.availablePaymentMethods[0],
|
|
412
|
+
paymentStatus: 'captured',
|
|
413
|
+
totals: session.totals,
|
|
414
|
+
adjustments: [],
|
|
415
|
+
returns: []
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// Mark session as completed
|
|
419
|
+
session.status = 'completed';
|
|
420
|
+
session.updatedAt = new Date().toISOString();
|
|
421
|
+
await this.storage.set(session);
|
|
422
|
+
|
|
423
|
+
return order;
|
|
424
|
+
|
|
425
|
+
} catch (error) {
|
|
426
|
+
// Revert to open status on failure
|
|
427
|
+
session.status = 'open';
|
|
428
|
+
session.updatedAt = new Date().toISOString();
|
|
429
|
+
await this.storage.set(session);
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// --------------------------------------------------------------------------
|
|
435
|
+
// AP2 Integration
|
|
436
|
+
// --------------------------------------------------------------------------
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Generate Cart Mandate for the session
|
|
440
|
+
*/
|
|
441
|
+
generateCartMandate(session: CheckoutSession): CartMandate {
|
|
442
|
+
return createCartMandate({
|
|
443
|
+
items: session.lineItems,
|
|
444
|
+
total: session.totals.total,
|
|
445
|
+
merchant: {
|
|
446
|
+
id: this.config.merchantId,
|
|
447
|
+
name: this.config.merchantName
|
|
448
|
+
},
|
|
449
|
+
paymentMethods: session.availablePaymentMethods.map(m => {
|
|
450
|
+
switch (m.type) {
|
|
451
|
+
case 'card': return 'CARD';
|
|
452
|
+
case 'bank_transfer': return 'BANK_TRANSFER';
|
|
453
|
+
case 'wallet': return 'WALLET';
|
|
454
|
+
case 'crypto': return 'CRYPTO';
|
|
455
|
+
default: return 'CARD';
|
|
456
|
+
}
|
|
457
|
+
}),
|
|
458
|
+
fulfillment: session.selectedFulfillment?.destination.address ? {
|
|
459
|
+
method: session.fulfillmentOptions.find(o => o.id === session.selectedFulfillment?.optionId)?.label || 'Standard',
|
|
460
|
+
destination: session.selectedFulfillment.destination.address
|
|
461
|
+
} : undefined,
|
|
462
|
+
refundPeriodDays: 30
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// --------------------------------------------------------------------------
|
|
467
|
+
// Helpers
|
|
468
|
+
// --------------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
private assertSessionOpen(session: CheckoutSession): void {
|
|
471
|
+
if (session.status !== 'open') {
|
|
472
|
+
throw new Error(`Session is ${session.status}, cannot modify`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private calculateTotals(
|
|
477
|
+
items: LineItem[],
|
|
478
|
+
discounts: DiscountAllocation[] = [],
|
|
479
|
+
fulfillment?: { optionId: string; destination: FulfillmentDestination },
|
|
480
|
+
shippingCost?: Money
|
|
481
|
+
): Totals {
|
|
482
|
+
const currency = items[0]?.unitPrice.currency || this.config.defaultCurrency;
|
|
483
|
+
|
|
484
|
+
const subtotal = items.reduce((sum, item) => sum + item.totalPrice.amount, 0);
|
|
485
|
+
const discountTotal = discounts.reduce((sum, d) => sum + d.appliedAmount.amount, 0);
|
|
486
|
+
const shipping = shippingCost?.amount || 0;
|
|
487
|
+
const tax = 0; // Calculated separately if tax calculator provided
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
subtotal: { amount: subtotal, currency },
|
|
491
|
+
discounts: { amount: discountTotal, currency },
|
|
492
|
+
shipping: { amount: shipping, currency },
|
|
493
|
+
tax: { amount: tax, currency },
|
|
494
|
+
total: { amount: subtotal - discountTotal + shipping + tax, currency }
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|