@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.
Files changed (169) hide show
  1. package/README.md +123 -0
  2. package/dist/auto-ui.d.ts +103 -0
  3. package/dist/auto-ui.d.ts.map +1 -0
  4. package/dist/auto-ui.js +275 -0
  5. package/dist/auto-ui.js.map +1 -0
  6. package/dist/base.d.ts +9 -2
  7. package/dist/base.d.ts.map +1 -1
  8. package/dist/base.js +23 -10
  9. package/dist/base.js.map +1 -1
  10. package/dist/cli-ui-renderer.d.ts +31 -0
  11. package/dist/cli-ui-renderer.d.ts.map +1 -0
  12. package/dist/cli-ui-renderer.js +224 -0
  13. package/dist/cli-ui-renderer.js.map +1 -0
  14. package/dist/dependency-manager.d.ts.map +1 -1
  15. package/dist/dependency-manager.js +0 -1
  16. package/dist/dependency-manager.js.map +1 -1
  17. package/dist/design-system/index.d.ts +21 -0
  18. package/dist/design-system/index.d.ts.map +1 -0
  19. package/dist/design-system/index.js +27 -0
  20. package/dist/design-system/index.js.map +1 -0
  21. package/dist/design-system/tokens.d.ts +149 -0
  22. package/dist/design-system/tokens.d.ts.map +1 -0
  23. package/dist/design-system/tokens.js +413 -0
  24. package/dist/design-system/tokens.js.map +1 -0
  25. package/dist/design-system/transaction-ui.d.ts +70 -0
  26. package/dist/design-system/transaction-ui.d.ts.map +1 -0
  27. package/dist/design-system/transaction-ui.js +982 -0
  28. package/dist/design-system/transaction-ui.js.map +1 -0
  29. package/dist/generator.d.ts +58 -8
  30. package/dist/generator.d.ts.map +1 -1
  31. package/dist/generator.js +9 -4
  32. package/dist/generator.js.map +1 -1
  33. package/dist/index.d.ts +10 -7
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +48 -44
  36. package/dist/index.js.map +1 -1
  37. package/dist/io.d.ts +395 -0
  38. package/dist/io.d.ts.map +1 -0
  39. package/dist/io.js +304 -0
  40. package/dist/io.js.map +1 -0
  41. package/dist/path-resolver.d.ts.map +1 -1
  42. package/dist/path-resolver.js +2 -1
  43. package/dist/path-resolver.js.map +1 -1
  44. package/dist/rendering/components.d.ts +29 -0
  45. package/dist/rendering/components.d.ts.map +1 -0
  46. package/dist/rendering/components.js +773 -0
  47. package/dist/rendering/components.js.map +1 -0
  48. package/dist/rendering/field-analyzer.d.ts +48 -0
  49. package/dist/rendering/field-analyzer.d.ts.map +1 -0
  50. package/dist/rendering/field-analyzer.js +270 -0
  51. package/dist/rendering/field-analyzer.js.map +1 -0
  52. package/dist/rendering/field-renderers.d.ts +64 -0
  53. package/dist/rendering/field-renderers.d.ts.map +1 -0
  54. package/dist/rendering/field-renderers.js +317 -0
  55. package/dist/rendering/field-renderers.js.map +1 -0
  56. package/dist/rendering/index.d.ts +28 -0
  57. package/dist/rendering/index.d.ts.map +1 -0
  58. package/dist/rendering/index.js +60 -0
  59. package/dist/rendering/index.js.map +1 -0
  60. package/dist/rendering/layout-selector.d.ts +48 -0
  61. package/dist/rendering/layout-selector.d.ts.map +1 -0
  62. package/dist/rendering/layout-selector.js +347 -0
  63. package/dist/rendering/layout-selector.js.map +1 -0
  64. package/dist/rendering/template-engine.d.ts +41 -0
  65. package/dist/rendering/template-engine.d.ts.map +1 -0
  66. package/dist/rendering/template-engine.js +236 -0
  67. package/dist/rendering/template-engine.js.map +1 -0
  68. package/dist/schema-extractor.d.ts +30 -0
  69. package/dist/schema-extractor.d.ts.map +1 -1
  70. package/dist/schema-extractor.js +205 -12
  71. package/dist/schema-extractor.js.map +1 -1
  72. package/dist/stateful.d.ts +63 -0
  73. package/dist/stateful.d.ts.map +1 -1
  74. package/dist/stateful.js +222 -0
  75. package/dist/stateful.js.map +1 -1
  76. package/dist/types.d.ts +9 -1
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/types.js.map +1 -1
  79. package/dist/ucp/ap2/handlers.d.ts +242 -0
  80. package/dist/ucp/ap2/handlers.d.ts.map +1 -0
  81. package/dist/ucp/ap2/handlers.js +482 -0
  82. package/dist/ucp/ap2/handlers.js.map +1 -0
  83. package/dist/ucp/ap2/mandates.d.ts +95 -0
  84. package/dist/ucp/ap2/mandates.d.ts.map +1 -0
  85. package/dist/ucp/ap2/mandates.js +234 -0
  86. package/dist/ucp/ap2/mandates.js.map +1 -0
  87. package/dist/ucp/ap2/types.d.ts +305 -0
  88. package/dist/ucp/ap2/types.d.ts.map +1 -0
  89. package/dist/ucp/ap2/types.js +8 -0
  90. package/dist/ucp/ap2/types.js.map +1 -0
  91. package/dist/ucp/capabilities/checkout.d.ts +118 -0
  92. package/dist/ucp/capabilities/checkout.d.ts.map +1 -0
  93. package/dist/ucp/capabilities/checkout.js +344 -0
  94. package/dist/ucp/capabilities/checkout.js.map +1 -0
  95. package/dist/ucp/capabilities/identity.d.ts +130 -0
  96. package/dist/ucp/capabilities/identity.d.ts.map +1 -0
  97. package/dist/ucp/capabilities/identity.js +290 -0
  98. package/dist/ucp/capabilities/identity.js.map +1 -0
  99. package/dist/ucp/capabilities/order.d.ts +142 -0
  100. package/dist/ucp/capabilities/order.d.ts.map +1 -0
  101. package/dist/ucp/capabilities/order.js +383 -0
  102. package/dist/ucp/capabilities/order.js.map +1 -0
  103. package/dist/ucp/index.d.ts +18 -0
  104. package/dist/ucp/index.d.ts.map +1 -0
  105. package/dist/ucp/index.js +19 -0
  106. package/dist/ucp/index.js.map +1 -0
  107. package/dist/ucp/manifest.d.ts +62 -0
  108. package/dist/ucp/manifest.d.ts.map +1 -0
  109. package/dist/ucp/manifest.js +180 -0
  110. package/dist/ucp/manifest.js.map +1 -0
  111. package/dist/ucp/types.d.ts +327 -0
  112. package/dist/ucp/types.d.ts.map +1 -0
  113. package/dist/ucp/types.js +8 -0
  114. package/dist/ucp/types.js.map +1 -0
  115. package/package.json +3 -4
  116. package/src/auto-ui.ts +413 -0
  117. package/src/base.ts +22 -9
  118. package/src/cli-ui-renderer.ts +264 -0
  119. package/src/dependency-manager.ts +0 -1
  120. package/src/design-system/index.ts +30 -0
  121. package/src/design-system/tokens.ts +451 -0
  122. package/src/design-system/transaction-ui.ts +1038 -0
  123. package/src/generator.ts +68 -8
  124. package/src/index.ts +159 -101
  125. package/src/io.ts +493 -0
  126. package/src/path-resolver.ts +2 -1
  127. package/src/rendering/components.ts +785 -0
  128. package/src/rendering/field-analyzer.ts +299 -0
  129. package/src/rendering/field-renderers.ts +356 -0
  130. package/src/rendering/index.ts +63 -0
  131. package/src/rendering/layout-selector.ts +390 -0
  132. package/src/rendering/template-engine.ts +254 -0
  133. package/src/schema-extractor.ts +225 -12
  134. package/src/stateful.ts +301 -0
  135. package/src/types.ts +10 -1
  136. package/src/ucp/ap2/handlers.ts +779 -0
  137. package/src/ucp/ap2/mandates.ts +354 -0
  138. package/src/ucp/ap2/types.ts +441 -0
  139. package/src/ucp/capabilities/checkout.ts +497 -0
  140. package/src/ucp/capabilities/identity.ts +425 -0
  141. package/src/ucp/capabilities/order.ts +549 -0
  142. package/src/ucp/index.ts +27 -0
  143. package/src/ucp/manifest.ts +257 -0
  144. package/src/ucp/types.ts +454 -0
  145. package/dist/cli-formatter.d.ts +0 -92
  146. package/dist/cli-formatter.d.ts.map +0 -1
  147. package/dist/cli-formatter.js +0 -486
  148. package/dist/cli-formatter.js.map +0 -1
  149. package/dist/elicit.d.ts +0 -93
  150. package/dist/elicit.d.ts.map +0 -1
  151. package/dist/elicit.js +0 -373
  152. package/dist/elicit.js.map +0 -1
  153. package/dist/mcp-client.d.ts +0 -218
  154. package/dist/mcp-client.d.ts.map +0 -1
  155. package/dist/mcp-client.js +0 -424
  156. package/dist/mcp-client.js.map +0 -1
  157. package/dist/mcp-sdk-transport.d.ts +0 -88
  158. package/dist/mcp-sdk-transport.d.ts.map +0 -1
  159. package/dist/mcp-sdk-transport.js +0 -360
  160. package/dist/mcp-sdk-transport.js.map +0 -1
  161. package/dist/photon-config.d.ts +0 -86
  162. package/dist/photon-config.d.ts.map +0 -1
  163. package/dist/photon-config.js +0 -156
  164. package/dist/photon-config.js.map +0 -1
  165. package/src/cli-formatter.ts +0 -579
  166. package/src/elicit.ts +0 -438
  167. package/src/mcp-client.ts +0 -561
  168. package/src/mcp-sdk-transport.ts +0 -449
  169. 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
+ }