payment-kit 1.24.3 → 1.25.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 (117) hide show
  1. package/api/src/crons/overdue-detection.ts +10 -1
  2. package/api/src/index.ts +3 -0
  3. package/api/src/libs/credit-utils.ts +21 -0
  4. package/api/src/libs/discount/discount.ts +13 -0
  5. package/api/src/libs/env.ts +5 -0
  6. package/api/src/libs/error.ts +14 -0
  7. package/api/src/libs/exchange-rate/coingecko-provider.ts +193 -0
  8. package/api/src/libs/exchange-rate/coinmarketcap-provider.ts +180 -0
  9. package/api/src/libs/exchange-rate/index.ts +5 -0
  10. package/api/src/libs/exchange-rate/service.ts +583 -0
  11. package/api/src/libs/exchange-rate/token-address-mapping.ts +84 -0
  12. package/api/src/libs/exchange-rate/token-addresses.json +2147 -0
  13. package/api/src/libs/exchange-rate/token-data-provider.ts +142 -0
  14. package/api/src/libs/exchange-rate/types.ts +114 -0
  15. package/api/src/libs/exchange-rate/validator.ts +319 -0
  16. package/api/src/libs/invoice-quote.ts +158 -0
  17. package/api/src/libs/invoice.ts +143 -7
  18. package/api/src/libs/math-utils.ts +46 -0
  19. package/api/src/libs/notification/template/billing-discrepancy.ts +3 -4
  20. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +174 -79
  21. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +2 -3
  22. package/api/src/libs/notification/template/customer-credit-insufficient.ts +3 -3
  23. package/api/src/libs/notification/template/customer-credit-low-balance.ts +3 -3
  24. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +2 -3
  25. package/api/src/libs/notification/template/customer-reward-succeeded.ts +9 -4
  26. package/api/src/libs/notification/template/exchange-rate-alert.ts +202 -0
  27. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +203 -0
  28. package/api/src/libs/notification/template/subscription-slippage-warning.ts +212 -0
  29. package/api/src/libs/notification/template/subscription-will-canceled.ts +2 -2
  30. package/api/src/libs/notification/template/subscription-will-renew.ts +22 -8
  31. package/api/src/libs/payment.ts +1 -1
  32. package/api/src/libs/price.ts +4 -1
  33. package/api/src/libs/queue/index.ts +8 -0
  34. package/api/src/libs/quote-service.ts +1132 -0
  35. package/api/src/libs/quote-validation.ts +388 -0
  36. package/api/src/libs/session.ts +686 -39
  37. package/api/src/libs/slippage.ts +135 -0
  38. package/api/src/libs/subscription.ts +185 -15
  39. package/api/src/libs/util.ts +64 -3
  40. package/api/src/locales/en.ts +50 -0
  41. package/api/src/locales/zh.ts +48 -0
  42. package/api/src/queues/auto-recharge.ts +295 -21
  43. package/api/src/queues/exchange-rate-health.ts +242 -0
  44. package/api/src/queues/invoice.ts +48 -1
  45. package/api/src/queues/notification.ts +190 -3
  46. package/api/src/queues/payment.ts +177 -7
  47. package/api/src/queues/subscription.ts +436 -6
  48. package/api/src/routes/auto-recharge-configs.ts +71 -6
  49. package/api/src/routes/checkout-sessions.ts +1730 -81
  50. package/api/src/routes/connect/auto-recharge-auth.ts +2 -0
  51. package/api/src/routes/connect/change-payer.ts +2 -0
  52. package/api/src/routes/connect/change-payment.ts +61 -8
  53. package/api/src/routes/connect/change-plan.ts +161 -17
  54. package/api/src/routes/connect/collect.ts +9 -6
  55. package/api/src/routes/connect/delegation.ts +1 -0
  56. package/api/src/routes/connect/pay.ts +157 -0
  57. package/api/src/routes/connect/setup.ts +32 -10
  58. package/api/src/routes/connect/shared.ts +159 -13
  59. package/api/src/routes/connect/subscribe.ts +32 -9
  60. package/api/src/routes/credit-grants.ts +99 -0
  61. package/api/src/routes/exchange-rate-providers.ts +248 -0
  62. package/api/src/routes/exchange-rates.ts +87 -0
  63. package/api/src/routes/index.ts +4 -0
  64. package/api/src/routes/invoices.ts +280 -2
  65. package/api/src/routes/meter-events.ts +3 -0
  66. package/api/src/routes/payment-links.ts +13 -0
  67. package/api/src/routes/prices.ts +84 -2
  68. package/api/src/routes/subscriptions.ts +526 -15
  69. package/api/src/store/migrations/20251220-dynamic-pricing.ts +245 -0
  70. package/api/src/store/migrations/20251223-exchange-rate-provider-type.ts +28 -0
  71. package/api/src/store/migrations/20260110-add-quote-locked-at.ts +23 -0
  72. package/api/src/store/migrations/20260112-add-checkout-session-slippage-percent.ts +22 -0
  73. package/api/src/store/migrations/20260113-add-price-quote-slippage-fields.ts +45 -0
  74. package/api/src/store/migrations/20260116-subscription-slippage.ts +21 -0
  75. package/api/src/store/migrations/20260120-auto-recharge-slippage.ts +21 -0
  76. package/api/src/store/models/auto-recharge-config.ts +12 -0
  77. package/api/src/store/models/checkout-session.ts +7 -0
  78. package/api/src/store/models/exchange-rate-provider.ts +225 -0
  79. package/api/src/store/models/index.ts +6 -0
  80. package/api/src/store/models/payment-intent.ts +6 -0
  81. package/api/src/store/models/price-quote.ts +284 -0
  82. package/api/src/store/models/price.ts +53 -5
  83. package/api/src/store/models/subscription.ts +11 -0
  84. package/api/src/store/models/types.ts +61 -1
  85. package/api/tests/libs/change-payment-plan.spec.ts +282 -0
  86. package/api/tests/libs/exchange-rate-service.spec.ts +341 -0
  87. package/api/tests/libs/quote-service.spec.ts +199 -0
  88. package/api/tests/libs/session.spec.ts +464 -0
  89. package/api/tests/libs/slippage.spec.ts +109 -0
  90. package/api/tests/libs/token-data-provider.spec.ts +267 -0
  91. package/api/tests/models/exchange-rate-provider.spec.ts +121 -0
  92. package/api/tests/models/price-dynamic.spec.ts +100 -0
  93. package/api/tests/models/price-quote.spec.ts +112 -0
  94. package/api/tests/routes/exchange-rate-providers.spec.ts +215 -0
  95. package/api/tests/routes/subscription-slippage.spec.ts +254 -0
  96. package/blocklet.yml +1 -1
  97. package/package.json +7 -6
  98. package/src/components/customer/credit-overview.tsx +14 -0
  99. package/src/components/discount/discount-info.tsx +8 -2
  100. package/src/components/invoice/list.tsx +146 -16
  101. package/src/components/invoice/table.tsx +276 -71
  102. package/src/components/invoice-pdf/template.tsx +3 -7
  103. package/src/components/metadata/form.tsx +6 -8
  104. package/src/components/price/form.tsx +519 -149
  105. package/src/components/promotion/active-redemptions.tsx +5 -3
  106. package/src/components/quote/info.tsx +234 -0
  107. package/src/hooks/subscription.ts +132 -2
  108. package/src/locales/en.tsx +145 -0
  109. package/src/locales/zh.tsx +143 -1
  110. package/src/pages/admin/billing/invoices/detail.tsx +41 -4
  111. package/src/pages/admin/products/exchange-rate-providers/edit-dialog.tsx +354 -0
  112. package/src/pages/admin/products/exchange-rate-providers/index.tsx +363 -0
  113. package/src/pages/admin/products/index.tsx +12 -1
  114. package/src/pages/customer/invoice/detail.tsx +36 -12
  115. package/src/pages/customer/subscription/change-payment.tsx +65 -3
  116. package/src/pages/customer/subscription/change-plan.tsx +207 -38
  117. package/src/pages/customer/subscription/detail.tsx +599 -419
@@ -34,6 +34,8 @@ import { MeterEvent, TMeterEvent } from './meter-event';
34
34
  import { AutoRechargeConfig } from './auto-recharge-config';
35
35
  import { ProductVendor } from './product-vendor';
36
36
  import { TaxRate } from './tax-rate';
37
+ import { ExchangeRateProvider } from './exchange-rate-provider';
38
+ import { PriceQuote } from './price-quote';
37
39
 
38
40
  const models = {
39
41
  CheckoutSession,
@@ -71,6 +73,8 @@ const models = {
71
73
  AutoRechargeConfig,
72
74
  ProductVendor,
73
75
  TaxRate,
76
+ ExchangeRateProvider,
77
+ PriceQuote,
74
78
  };
75
79
 
76
80
  export function initialize(sequelize: any) {
@@ -122,6 +126,8 @@ export * from './meter-event';
122
126
  export * from './auto-recharge-config';
123
127
  export * from './product-vendor';
124
128
  export * from './tax-rate';
129
+ export * from './exchange-rate-provider';
130
+ export * from './price-quote';
125
131
 
126
132
  export type TPriceExpanded = TPrice & {
127
133
  object: 'price';
@@ -80,6 +80,8 @@ export class PaymentIntent extends Model<InferAttributes<PaymentIntent>, InferCr
80
80
 
81
81
  declare setup_future_usage?: LiteralUnion<'off_session' | 'on_session', string>;
82
82
 
83
+ declare quote_locked_at?: Date;
84
+
83
85
  // how to split the payment from this link
84
86
  declare beneficiaries?: PaymentBeneficiary[];
85
87
 
@@ -222,6 +224,10 @@ export class PaymentIntent extends Model<InferAttributes<PaymentIntent>, InferCr
222
224
  type: DataTypes.ENUM('on_session', 'off_session'),
223
225
  allowNull: true,
224
226
  },
227
+ quote_locked_at: {
228
+ type: DataTypes.DATE,
229
+ allowNull: true,
230
+ },
225
231
  created_at: {
226
232
  type: DataTypes.DATE,
227
233
  defaultValue: DataTypes.NOW,
@@ -0,0 +1,284 @@
1
+ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model, Transaction } from 'sequelize';
2
+ import type { LiteralUnion } from 'type-fest';
3
+
4
+ import { createEvent } from '../../libs/audit';
5
+ import { createIdGenerator } from '../../libs/util';
6
+ import { QuoteMetadata, QuoteStatus } from './types';
7
+
8
+ export const nextPriceQuoteId = createIdGenerator('pq', 24);
9
+
10
+ /**
11
+ * Create Quote Input (Final Freeze Architecture)
12
+ *
13
+ * Quote is created only at Submit time, directly as 'used' status.
14
+ * @see Intent: blocklets/core/ai/intent/20260112-dynamic-price.md
15
+ */
16
+ export interface CreateQuoteInput {
17
+ price_id: string;
18
+ session_id?: string;
19
+ invoice_id?: string;
20
+ idempotency_key: string;
21
+ base_currency?: string;
22
+ base_amount: string;
23
+ target_currency_id: string;
24
+ rate_currency_symbol: string;
25
+ exchange_rate: string;
26
+ quoted_amount: string;
27
+ rate_provider_id: string;
28
+ rate_provider_name: string;
29
+ rate_timestamp_ms: number;
30
+ slippage_percent?: number;
31
+ max_payable_token?: string;
32
+ min_acceptable_rate?: string;
33
+ metadata?: QuoteMetadata;
34
+ }
35
+
36
+ export class PriceQuote extends Model<InferAttributes<PriceQuote>, InferCreationAttributes<PriceQuote>> {
37
+ declare id: CreationOptional<string>;
38
+ declare price_id: string;
39
+ declare session_id?: string;
40
+ declare invoice_id?: string;
41
+ declare idempotency_key: string;
42
+
43
+ // Base currency (always USD in P0)
44
+ declare base_currency: string;
45
+ declare base_amount: string;
46
+
47
+ // Target currency and rate
48
+ declare target_currency_id: string;
49
+ declare rate_currency_symbol: string;
50
+ declare exchange_rate: string; // USD per token
51
+ declare quoted_amount: string; // in smallest unit
52
+
53
+ // Rate provider info
54
+ declare rate_provider_id: string;
55
+ declare rate_provider_name: string;
56
+ declare rate_timestamp_ms: number;
57
+
58
+ // Lifecycle
59
+ declare expires_at: number; // Unix timestamp
60
+ declare status: CreationOptional<LiteralUnion<QuoteStatus, string>>;
61
+
62
+ // Metadata
63
+ declare metadata: QuoteMetadata | null;
64
+
65
+ // Slippage snapshot (derived when quote is used)
66
+ declare slippage_percent?: number;
67
+ declare max_payable_token?: string;
68
+ declare min_acceptable_rate?: string;
69
+ declare slippage_derived_at_ms?: number;
70
+
71
+ declare created_at: CreationOptional<Date>;
72
+
73
+ public static initialize(sequelize: any) {
74
+ this.init(
75
+ {
76
+ id: {
77
+ type: DataTypes.STRING(32),
78
+ primaryKey: true,
79
+ allowNull: false,
80
+ defaultValue: nextPriceQuoteId,
81
+ },
82
+ price_id: {
83
+ type: DataTypes.STRING(32),
84
+ allowNull: false,
85
+ },
86
+ session_id: {
87
+ type: DataTypes.STRING(32),
88
+ allowNull: true,
89
+ },
90
+ invoice_id: {
91
+ type: DataTypes.STRING(32),
92
+ allowNull: true,
93
+ },
94
+ idempotency_key: {
95
+ type: DataTypes.STRING(128),
96
+ allowNull: false,
97
+ unique: true,
98
+ },
99
+ base_currency: {
100
+ type: DataTypes.STRING(8),
101
+ allowNull: false,
102
+ defaultValue: 'USD',
103
+ },
104
+ base_amount: {
105
+ type: DataTypes.STRING(32),
106
+ allowNull: false,
107
+ },
108
+ target_currency_id: {
109
+ type: DataTypes.STRING(16),
110
+ allowNull: false,
111
+ },
112
+ rate_currency_symbol: {
113
+ type: DataTypes.STRING(16),
114
+ allowNull: false,
115
+ },
116
+ exchange_rate: {
117
+ type: DataTypes.STRING(32),
118
+ allowNull: false,
119
+ },
120
+ quoted_amount: {
121
+ type: DataTypes.STRING(64),
122
+ allowNull: false,
123
+ },
124
+ rate_provider_id: {
125
+ type: DataTypes.STRING(32),
126
+ allowNull: false,
127
+ },
128
+ rate_provider_name: {
129
+ type: DataTypes.STRING(64),
130
+ allowNull: false,
131
+ },
132
+ rate_timestamp_ms: {
133
+ type: DataTypes.BIGINT,
134
+ allowNull: false,
135
+ },
136
+ expires_at: {
137
+ type: DataTypes.INTEGER,
138
+ allowNull: false,
139
+ },
140
+ status: {
141
+ // Final Freeze: Only 3 statuses - used/paid/payment_failed
142
+ type: DataTypes.ENUM('used', 'paid', 'payment_failed'),
143
+ allowNull: false,
144
+ defaultValue: 'used',
145
+ },
146
+ metadata: {
147
+ type: DataTypes.JSON,
148
+ allowNull: true,
149
+ },
150
+ slippage_percent: {
151
+ type: DataTypes.DECIMAL(5, 2),
152
+ allowNull: true,
153
+ },
154
+ max_payable_token: {
155
+ type: DataTypes.STRING(64),
156
+ allowNull: true,
157
+ },
158
+ min_acceptable_rate: {
159
+ type: DataTypes.STRING(32),
160
+ allowNull: true,
161
+ },
162
+ slippage_derived_at_ms: {
163
+ type: DataTypes.BIGINT,
164
+ allowNull: true,
165
+ },
166
+ created_at: {
167
+ type: DataTypes.DATE,
168
+ defaultValue: DataTypes.NOW,
169
+ allowNull: false,
170
+ },
171
+ },
172
+ {
173
+ sequelize,
174
+ modelName: 'PriceQuote',
175
+ tableName: 'price_quotes',
176
+ createdAt: 'created_at',
177
+ updatedAt: false,
178
+ hooks: {
179
+ afterCreate: (model: PriceQuote, options) => {
180
+ // Async event creation - don't block transaction
181
+ if (options.transaction) {
182
+ // Defer until after transaction commits
183
+ options.transaction.afterCommit(() => {
184
+ createEvent('PriceQuote', 'price_quote.created', model, options).catch(console.error);
185
+ });
186
+ } else {
187
+ // No transaction, create immediately
188
+ createEvent('PriceQuote', 'price_quote.created', model, options).catch(console.error);
189
+ }
190
+ },
191
+ afterUpdate: (model: PriceQuote, options) => {
192
+ // Async event creation - don't block transaction
193
+ if (options.transaction) {
194
+ // Defer until after transaction commits
195
+ options.transaction.afterCommit(() => {
196
+ createEvent('PriceQuote', 'price_quote.updated', model, options).catch(console.error);
197
+ });
198
+ } else {
199
+ // No transaction, create immediately
200
+ createEvent('PriceQuote', 'price_quote.updated', model, options).catch(console.error);
201
+ }
202
+ },
203
+ },
204
+ }
205
+ );
206
+ }
207
+
208
+ public static associate(models: any) {
209
+ this.belongsTo(models.Price, {
210
+ foreignKey: 'price_id',
211
+ as: 'price',
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Check if Quote is usable for payment
217
+ * Final Freeze: 'used' status means Quote is ready for payment
218
+ */
219
+ public isUsable(): boolean {
220
+ return this.status === 'used';
221
+ }
222
+
223
+ /**
224
+ * Check if Quote can be retried (payment_failed can be retried)
225
+ */
226
+ public canRetry(): boolean {
227
+ return this.status === 'used' || this.status === 'payment_failed';
228
+ }
229
+
230
+ /**
231
+ * Find Quote by idempotency key (for idempotent Submit)
232
+ * Returns any Quote with the given key regardless of status
233
+ */
234
+ public static findByIdempotencyKey(idempotencyKey: string): Promise<PriceQuote | null> {
235
+ return this.findOne({
236
+ where: {
237
+ idempotency_key: idempotencyKey,
238
+ },
239
+ });
240
+ }
241
+
242
+ /**
243
+ * Create Quote directly as 'used' (Final Freeze architecture)
244
+ *
245
+ * This is the ONLY way to create Quotes in the new architecture.
246
+ * Quotes are created at Submit time, never during Preview.
247
+ */
248
+ public static async createUsedQuote(input: CreateQuoteInput, transaction?: Transaction): Promise<PriceQuote> {
249
+ const now = Math.floor(Date.now() / 1000);
250
+ const quote = await this.create(
251
+ {
252
+ ...input,
253
+ base_currency: input.base_currency || 'USD',
254
+ status: 'used',
255
+ // expires_at kept for backward compatibility but not used for validation
256
+ expires_at: now + 180, // 3 minutes (flow SLA, not Quote validity)
257
+ slippage_derived_at_ms: Date.now(),
258
+ },
259
+ { transaction }
260
+ );
261
+ return quote;
262
+ }
263
+
264
+ /**
265
+ * Mark Quote as paid (payment completed successfully)
266
+ * Idempotent: if already paid, returns silently
267
+ */
268
+ public async markAsPaid(transaction?: any): Promise<void> {
269
+ if (this.status === 'paid') {
270
+ return;
271
+ }
272
+ await this.update({ status: 'paid' }, { transaction });
273
+ }
274
+
275
+ /**
276
+ * Mark Quote as payment_failed
277
+ * Note: payment_failed does NOT mean Quote is invalid, can be retried
278
+ */
279
+ public async markAsPaymentFailed(transaction?: any): Promise<void> {
280
+ await this.update({ status: 'payment_failed' }, { transaction });
281
+ }
282
+ }
283
+
284
+ export type TPriceQuote = InferAttributes<PriceQuote>;
@@ -101,6 +101,12 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
101
101
 
102
102
  declare tax_behavior?: LiteralUnion<'inclusive' | 'exclusive', string>;
103
103
 
104
+ // Dynamic pricing fields
105
+ declare pricing_type: LiteralUnion<'fixed' | 'dynamic', string>;
106
+ declare base_currency: string | null;
107
+ declare base_amount: string | null;
108
+ declare dynamic_pricing_config: { lock_duration?: number } | null;
109
+
104
110
  public static readonly GENESIS_ATTRIBUTES = {
105
111
  id: {
106
112
  type: DataTypes.STRING(32),
@@ -218,6 +224,23 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
218
224
  allowNull: false,
219
225
  defaultValue: 'inclusive',
220
226
  },
227
+ pricing_type: {
228
+ type: DataTypes.ENUM('fixed', 'dynamic'),
229
+ allowNull: false,
230
+ defaultValue: 'fixed',
231
+ },
232
+ base_currency: {
233
+ type: DataTypes.STRING(8),
234
+ allowNull: true,
235
+ },
236
+ base_amount: {
237
+ type: DataTypes.STRING(32),
238
+ allowNull: true,
239
+ },
240
+ dynamic_pricing_config: {
241
+ type: DataTypes.JSON,
242
+ allowNull: true,
243
+ },
221
244
  },
222
245
  {
223
246
  sequelize,
@@ -273,6 +296,25 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
273
296
  }
274
297
 
275
298
  public static formatBeforeSave(price: Partial<TPrice & { model: string }>) {
299
+ // Validate dynamic pricing fields
300
+ if (price.pricing_type === 'dynamic') {
301
+ if (!price.base_currency || !price.base_amount) {
302
+ throw new Error('base_currency and base_amount are required for dynamic pricing');
303
+ }
304
+ if (price.base_currency !== 'USD') {
305
+ throw new Error('Only USD is supported as base_currency');
306
+ }
307
+ // Set default lock_duration if not provided
308
+ if (!price.dynamic_pricing_config) {
309
+ price.dynamic_pricing_config = { lock_duration: 30 };
310
+ }
311
+ } else if (price.pricing_type === 'fixed') {
312
+ // Clear dynamic pricing fields for fixed prices
313
+ price.base_currency = null;
314
+ price.base_amount = null;
315
+ price.dynamic_pricing_config = null;
316
+ }
317
+
276
318
  if (price.type) {
277
319
  if (price.type === 'recurring') {
278
320
  if (!price.recurring) {
@@ -413,11 +455,17 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
413
455
  }
414
456
 
415
457
  // @ts-ignore
416
- return items.map((x) => ({
417
- ...x,
418
- price: prices.find((p) => p.id === x.price_id),
419
- upsell_price: x.upsell_price_id ? prices.find((p) => p.id === x.upsell_price_id) : null,
420
- })) as TLineItemExpanded[];
458
+ return items.map((x) => {
459
+ const price = prices.find((p) => p.id === x.price_id);
460
+ if (!price) {
461
+ logger.warn('Price not found for line item', { priceId: x.price_id });
462
+ }
463
+ return {
464
+ ...x,
465
+ price,
466
+ upsell_price: x.upsell_price_id ? prices.find((p) => p.id === x.upsell_price_id) : null,
467
+ };
468
+ }) as TLineItemExpanded[];
421
469
  }
422
470
 
423
471
  public static async insert(price: TPrice & { model: string }) {
@@ -114,6 +114,13 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
114
114
 
115
115
  // 3rd party payment tx hash
116
116
  declare payment_details?: PaymentDetails;
117
+ declare slippage_config?: {
118
+ mode?: 'percent' | 'rate';
119
+ percent?: number;
120
+ min_acceptable_rate?: string;
121
+ base_currency?: string;
122
+ updated_at_ms?: number;
123
+ } | null;
117
124
 
118
125
  declare proration_behavior?: LiteralUnion<'always_invoice' | 'create_prorations' | 'none', string>;
119
126
  declare payment_behavior?: LiteralUnion<'allow_incomplete' | 'error_if_incomplete' | 'pending_if_incomplete', string>;
@@ -223,6 +230,10 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
223
230
  type: DataTypes.ENUM('charge_automatically', 'send_invoice'),
224
231
  allowNull: false,
225
232
  },
233
+ slippage_config: {
234
+ type: DataTypes.JSON,
235
+ allowNull: true,
236
+ },
226
237
  days_until_due: {
227
238
  type: DataTypes.NUMBER,
228
239
  allowNull: true,
@@ -730,7 +730,9 @@ export type EventType = LiteralUnion<
730
730
  | 'customer.credit.insufficient'
731
731
  | 'customer.credit.low_balance'
732
732
  | 'customer.credit_grant.granted'
733
- | 'customer.credit_grant.depleted',
733
+ | 'customer.credit_grant.depleted'
734
+ | 'exchange_rate.providers_unavailable'
735
+ | 'exchange_rate.spread_exceeded',
734
736
  string
735
737
  >;
736
738
 
@@ -925,3 +927,61 @@ export type CreditGrantChainDetail = {
925
927
  // Voided reason
926
928
  voided_reason?: 'refund' | 'expired' | 'manual';
927
929
  };
930
+
931
+ /**
932
+ * Quote Status (Final Freeze)
933
+ *
934
+ * - used: Quote created and consumed at Submit
935
+ * - paid: Payment completed successfully
936
+ * - payment_failed: Payment flow failed (does NOT mean Quote is invalid)
937
+ *
938
+ * Legacy statuses (deprecated, for backward compatibility only):
939
+ * - active, expired, cancelled, failed
940
+ *
941
+ * @see Intent: blocklets/core/ai/intent/20260112-dynamic-price.md
942
+ */
943
+ /**
944
+ * Final Freeze: Quote only has 3 statuses
945
+ * - used: Quote created at Submit, ready for payment
946
+ * - paid: Payment completed successfully
947
+ * - payment_failed: Payment process failed (Quote still valid for retry)
948
+ */
949
+ export type QuoteStatus = 'used' | 'paid' | 'payment_failed';
950
+
951
+ export interface QuoteMetadata {
952
+ calculation?: {
953
+ token_amount_raw?: string; // Deprecated: use total_base_amount_scaled + rate_scaled instead
954
+ unit_amount_raw?: string; // Deprecated: use quoted_amount_unit instead
955
+ total_base_amount_scaled?: string; // BN: total USD amount scaled by 10^8
956
+ rate_scaled?: string; // BN: exchange rate scaled by 10^8
957
+ quoted_amount_unit?: string; // BN: final quoted amount in smallest token unit
958
+ };
959
+ rounding?: {
960
+ mode?: string;
961
+ token_decimals?: number;
962
+ };
963
+ risk?: {
964
+ anomaly_detected?: boolean;
965
+ deviation_percent?: number;
966
+ degraded?: boolean;
967
+ degraded_reason?: string | null;
968
+ };
969
+ rate?: {
970
+ consensus_method?: string;
971
+ providers?: Array<{
972
+ id?: string;
973
+ name?: string;
974
+ rate?: string;
975
+ timestamp_ms?: number;
976
+ degraded?: boolean;
977
+ degraded_reason?: string;
978
+ }>;
979
+ };
980
+ slippage?: {
981
+ percent?: number;
982
+ max_payable_token?: string;
983
+ min_acceptable_rate?: string;
984
+ derived_at_ms?: number;
985
+ };
986
+ context?: Record<string, any>;
987
+ }