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.
- package/api/src/crons/overdue-detection.ts +10 -1
- package/api/src/index.ts +3 -0
- package/api/src/libs/credit-utils.ts +21 -0
- package/api/src/libs/discount/discount.ts +13 -0
- package/api/src/libs/env.ts +5 -0
- package/api/src/libs/error.ts +14 -0
- package/api/src/libs/exchange-rate/coingecko-provider.ts +193 -0
- package/api/src/libs/exchange-rate/coinmarketcap-provider.ts +180 -0
- package/api/src/libs/exchange-rate/index.ts +5 -0
- package/api/src/libs/exchange-rate/service.ts +583 -0
- package/api/src/libs/exchange-rate/token-address-mapping.ts +84 -0
- package/api/src/libs/exchange-rate/token-addresses.json +2147 -0
- package/api/src/libs/exchange-rate/token-data-provider.ts +142 -0
- package/api/src/libs/exchange-rate/types.ts +114 -0
- package/api/src/libs/exchange-rate/validator.ts +319 -0
- package/api/src/libs/invoice-quote.ts +158 -0
- package/api/src/libs/invoice.ts +143 -7
- package/api/src/libs/math-utils.ts +46 -0
- package/api/src/libs/notification/template/billing-discrepancy.ts +3 -4
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +174 -79
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +2 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +3 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +3 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +2 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +9 -4
- package/api/src/libs/notification/template/exchange-rate-alert.ts +202 -0
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +203 -0
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +212 -0
- package/api/src/libs/notification/template/subscription-will-canceled.ts +2 -2
- package/api/src/libs/notification/template/subscription-will-renew.ts +22 -8
- package/api/src/libs/payment.ts +1 -1
- package/api/src/libs/price.ts +4 -1
- package/api/src/libs/queue/index.ts +8 -0
- package/api/src/libs/quote-service.ts +1132 -0
- package/api/src/libs/quote-validation.ts +388 -0
- package/api/src/libs/session.ts +686 -39
- package/api/src/libs/slippage.ts +135 -0
- package/api/src/libs/subscription.ts +185 -15
- package/api/src/libs/util.ts +64 -3
- package/api/src/locales/en.ts +50 -0
- package/api/src/locales/zh.ts +48 -0
- package/api/src/queues/auto-recharge.ts +295 -21
- package/api/src/queues/exchange-rate-health.ts +242 -0
- package/api/src/queues/invoice.ts +48 -1
- package/api/src/queues/notification.ts +190 -3
- package/api/src/queues/payment.ts +177 -7
- package/api/src/queues/subscription.ts +436 -6
- package/api/src/routes/auto-recharge-configs.ts +71 -6
- package/api/src/routes/checkout-sessions.ts +1730 -81
- package/api/src/routes/connect/auto-recharge-auth.ts +2 -0
- package/api/src/routes/connect/change-payer.ts +2 -0
- package/api/src/routes/connect/change-payment.ts +61 -8
- package/api/src/routes/connect/change-plan.ts +161 -17
- package/api/src/routes/connect/collect.ts +9 -6
- package/api/src/routes/connect/delegation.ts +1 -0
- package/api/src/routes/connect/pay.ts +157 -0
- package/api/src/routes/connect/setup.ts +32 -10
- package/api/src/routes/connect/shared.ts +159 -13
- package/api/src/routes/connect/subscribe.ts +32 -9
- package/api/src/routes/credit-grants.ts +99 -0
- package/api/src/routes/exchange-rate-providers.ts +248 -0
- package/api/src/routes/exchange-rates.ts +87 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/invoices.ts +280 -2
- package/api/src/routes/meter-events.ts +3 -0
- package/api/src/routes/payment-links.ts +13 -0
- package/api/src/routes/prices.ts +84 -2
- package/api/src/routes/subscriptions.ts +526 -15
- package/api/src/store/migrations/20251220-dynamic-pricing.ts +245 -0
- package/api/src/store/migrations/20251223-exchange-rate-provider-type.ts +28 -0
- package/api/src/store/migrations/20260110-add-quote-locked-at.ts +23 -0
- package/api/src/store/migrations/20260112-add-checkout-session-slippage-percent.ts +22 -0
- package/api/src/store/migrations/20260113-add-price-quote-slippage-fields.ts +45 -0
- package/api/src/store/migrations/20260116-subscription-slippage.ts +21 -0
- package/api/src/store/migrations/20260120-auto-recharge-slippage.ts +21 -0
- package/api/src/store/models/auto-recharge-config.ts +12 -0
- package/api/src/store/models/checkout-session.ts +7 -0
- package/api/src/store/models/exchange-rate-provider.ts +225 -0
- package/api/src/store/models/index.ts +6 -0
- package/api/src/store/models/payment-intent.ts +6 -0
- package/api/src/store/models/price-quote.ts +284 -0
- package/api/src/store/models/price.ts +53 -5
- package/api/src/store/models/subscription.ts +11 -0
- package/api/src/store/models/types.ts +61 -1
- package/api/tests/libs/change-payment-plan.spec.ts +282 -0
- package/api/tests/libs/exchange-rate-service.spec.ts +341 -0
- package/api/tests/libs/quote-service.spec.ts +199 -0
- package/api/tests/libs/session.spec.ts +464 -0
- package/api/tests/libs/slippage.spec.ts +109 -0
- package/api/tests/libs/token-data-provider.spec.ts +267 -0
- package/api/tests/models/exchange-rate-provider.spec.ts +121 -0
- package/api/tests/models/price-dynamic.spec.ts +100 -0
- package/api/tests/models/price-quote.spec.ts +112 -0
- package/api/tests/routes/exchange-rate-providers.spec.ts +215 -0
- package/api/tests/routes/subscription-slippage.spec.ts +254 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -6
- package/src/components/customer/credit-overview.tsx +14 -0
- package/src/components/discount/discount-info.tsx +8 -2
- package/src/components/invoice/list.tsx +146 -16
- package/src/components/invoice/table.tsx +276 -71
- package/src/components/invoice-pdf/template.tsx +3 -7
- package/src/components/metadata/form.tsx +6 -8
- package/src/components/price/form.tsx +519 -149
- package/src/components/promotion/active-redemptions.tsx +5 -3
- package/src/components/quote/info.tsx +234 -0
- package/src/hooks/subscription.ts +132 -2
- package/src/locales/en.tsx +145 -0
- package/src/locales/zh.tsx +143 -1
- package/src/pages/admin/billing/invoices/detail.tsx +41 -4
- package/src/pages/admin/products/exchange-rate-providers/edit-dialog.tsx +354 -0
- package/src/pages/admin/products/exchange-rate-providers/index.tsx +363 -0
- package/src/pages/admin/products/index.tsx +12 -1
- package/src/pages/customer/invoice/detail.tsx +36 -12
- package/src/pages/customer/subscription/change-payment.tsx +65 -3
- package/src/pages/customer/subscription/change-plan.tsx +207 -38
- 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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
+
}
|