payment-kit 1.20.10 → 1.20.12
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 +25 -24
- package/api/src/index.ts +2 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +63 -5
- package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -0
- package/api/src/integrations/stripe/resource.ts +253 -2
- package/api/src/libs/currency.ts +31 -0
- package/api/src/libs/discount/coupon.ts +1061 -0
- package/api/src/libs/discount/discount.ts +349 -0
- package/api/src/libs/discount/nft.ts +239 -0
- package/api/src/libs/discount/redemption.ts +636 -0
- package/api/src/libs/discount/vc.ts +73 -0
- package/api/src/libs/invoice.ts +50 -16
- package/api/src/libs/math-utils.ts +6 -0
- package/api/src/libs/price.ts +43 -0
- package/api/src/libs/session.ts +242 -57
- package/api/src/libs/subscription.ts +2 -6
- package/api/src/locales/en.ts +38 -38
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/discount-status.ts +200 -0
- package/api/src/queues/subscription.ts +98 -5
- package/api/src/queues/usage-record.ts +1 -1
- package/api/src/routes/auto-recharge-configs.ts +5 -3
- package/api/src/routes/checkout-sessions.ts +755 -64
- package/api/src/routes/connect/change-payment.ts +6 -1
- package/api/src/routes/connect/change-plan.ts +6 -1
- package/api/src/routes/connect/setup.ts +6 -1
- package/api/src/routes/connect/shared.ts +80 -9
- package/api/src/routes/connect/subscribe.ts +12 -2
- package/api/src/routes/coupons.ts +518 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/invoices.ts +44 -3
- package/api/src/routes/meter-events.ts +2 -1
- package/api/src/routes/payment-currencies.ts +1 -0
- package/api/src/routes/promotion-codes.ts +482 -0
- package/api/src/routes/subscriptions.ts +23 -2
- package/api/src/store/migrations/20250904-discount.ts +136 -0
- package/api/src/store/migrations/20250910-timestamp-fields.ts +116 -0
- package/api/src/store/migrations/20250916-add-description-fields.ts +30 -0
- package/api/src/store/models/checkout-session.ts +12 -0
- package/api/src/store/models/coupon.ts +144 -4
- package/api/src/store/models/discount.ts +23 -10
- package/api/src/store/models/index.ts +13 -2
- package/api/src/store/models/promotion-code.ts +295 -18
- package/api/src/store/models/types.ts +30 -1
- package/api/tests/libs/session.spec.ts +48 -27
- package/blocklet.yml +1 -1
- package/doc/vendor_fulfillment_system.md +38 -38
- package/package.json +20 -20
- package/src/app.tsx +2 -0
- package/src/components/customer/link.tsx +1 -1
- package/src/components/discount/discount-info.tsx +178 -0
- package/src/components/invoice/table.tsx +140 -48
- package/src/components/invoice-pdf/styles.ts +6 -0
- package/src/components/invoice-pdf/template.tsx +59 -33
- package/src/components/metadata/form.tsx +14 -5
- package/src/components/payment-link/actions.tsx +42 -0
- package/src/components/price/form.tsx +91 -65
- package/src/components/product/vendor-config.tsx +5 -3
- package/src/components/promotion/active-redemptions.tsx +534 -0
- package/src/components/promotion/currency-multi-select.tsx +350 -0
- package/src/components/promotion/currency-restrictions.tsx +117 -0
- package/src/components/promotion/product-select.tsx +292 -0
- package/src/components/promotion/promotion-code-form.tsx +534 -0
- package/src/components/subscription/portal/list.tsx +6 -1
- package/src/components/subscription/vendor-service-list.tsx +13 -2
- package/src/locales/en.tsx +253 -26
- package/src/locales/zh.tsx +222 -1
- package/src/pages/admin/billing/subscriptions/detail.tsx +5 -0
- package/src/pages/admin/products/coupons/applicable-products.tsx +166 -0
- package/src/pages/admin/products/coupons/create.tsx +612 -0
- package/src/pages/admin/products/coupons/detail.tsx +538 -0
- package/src/pages/admin/products/coupons/edit.tsx +127 -0
- package/src/pages/admin/products/coupons/index.tsx +210 -3
- package/src/pages/admin/products/index.tsx +22 -3
- package/src/pages/admin/products/products/detail.tsx +12 -2
- package/src/pages/admin/products/promotion-codes/actions.tsx +103 -0
- package/src/pages/admin/products/promotion-codes/create.tsx +235 -0
- package/src/pages/admin/products/promotion-codes/detail.tsx +416 -0
- package/src/pages/admin/products/promotion-codes/list.tsx +247 -0
- package/src/pages/admin/products/promotion-codes/verification-config.tsx +327 -0
- package/src/pages/admin/products/vendors/index.tsx +17 -5
- package/src/pages/customer/subscription/detail.tsx +5 -0
- package/vite.config.ts +4 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { Migration } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
const isColumnInteger = async (tableName: string, columnName: string): Promise<boolean> => {
|
|
6
|
+
const [results] = await context.sequelize.query(`PRAGMA table_info(${tableName})`);
|
|
7
|
+
const columnInfo = (results as any[]).find((col: any) => col.name === columnName);
|
|
8
|
+
return columnInfo && columnInfo.type.toUpperCase() === 'INTEGER';
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const migrateDateToInteger = async (
|
|
12
|
+
tableName: string,
|
|
13
|
+
columnName: string,
|
|
14
|
+
options: { allowNull: boolean; convertAllRows?: boolean } = { allowNull: true }
|
|
15
|
+
) => {
|
|
16
|
+
if (await isColumnInteger(tableName, columnName)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const tempColumnName = `${columnName}_new`;
|
|
21
|
+
|
|
22
|
+
await context.addColumn(tableName, tempColumnName, {
|
|
23
|
+
type: DataTypes.INTEGER,
|
|
24
|
+
allowNull: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const updateQuery = options.convertAllRows
|
|
28
|
+
? `UPDATE ${tableName} SET ${tempColumnName} = strftime('%s', ${columnName})`
|
|
29
|
+
: `UPDATE ${tableName} SET ${tempColumnName} = strftime('%s', ${columnName}) WHERE ${columnName} IS NOT NULL`;
|
|
30
|
+
|
|
31
|
+
await context.sequelize.query(updateQuery);
|
|
32
|
+
|
|
33
|
+
// Replace old column
|
|
34
|
+
await context.removeColumn(tableName, columnName);
|
|
35
|
+
await context.renameColumn(tableName, tempColumnName, columnName);
|
|
36
|
+
|
|
37
|
+
// Apply final column type and constraints
|
|
38
|
+
await context.changeColumn(tableName, columnName, {
|
|
39
|
+
type: DataTypes.INTEGER,
|
|
40
|
+
allowNull: options.allowNull,
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Define columns to migrate
|
|
45
|
+
const migrations = [
|
|
46
|
+
{ table: 'promotion_codes', column: 'expires_at', allowNull: true },
|
|
47
|
+
{ table: 'coupons', column: 'redeem_by', allowNull: true },
|
|
48
|
+
{ table: 'discounts', column: 'start', allowNull: false, convertAllRows: true },
|
|
49
|
+
{ table: 'discounts', column: 'end', allowNull: true },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Execute migrations
|
|
53
|
+
for (const migration of migrations) {
|
|
54
|
+
// eslint-disable-next-line no-await-in-loop
|
|
55
|
+
await migrateDateToInteger(migration.table, migration.column, {
|
|
56
|
+
allowNull: migration.allowNull,
|
|
57
|
+
convertAllRows: migration.convertAllRows,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const down: Migration = async ({ context }) => {
|
|
63
|
+
const isColumnDate = async (tableName: string, columnName: string): Promise<boolean> => {
|
|
64
|
+
const [results] = await context.sequelize.query(`PRAGMA table_info(${tableName})`);
|
|
65
|
+
const columnInfo = (results as any[]).find((col: any) => col.name === columnName);
|
|
66
|
+
return columnInfo && (columnInfo.type.toUpperCase() === 'DATE' || columnInfo.type.toUpperCase() === 'DATETIME');
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const migrateIntegerToDate = async (
|
|
70
|
+
tableName: string,
|
|
71
|
+
columnName: string,
|
|
72
|
+
options: { allowNull: boolean; convertAllRows?: boolean } = { allowNull: true }
|
|
73
|
+
) => {
|
|
74
|
+
if (await isColumnDate(tableName, columnName)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const tempColumnName = `${columnName}_old`;
|
|
79
|
+
|
|
80
|
+
// Add temporary DATE column
|
|
81
|
+
await context.addColumn(tableName, tempColumnName, {
|
|
82
|
+
type: DataTypes.DATE,
|
|
83
|
+
allowNull: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const updateQuery = options.convertAllRows
|
|
87
|
+
? `UPDATE ${tableName} SET ${tempColumnName} = datetime(${columnName}, 'unixepoch')`
|
|
88
|
+
: `UPDATE ${tableName} SET ${tempColumnName} = datetime(${columnName}, 'unixepoch') WHERE ${columnName} IS NOT NULL`;
|
|
89
|
+
|
|
90
|
+
await context.sequelize.query(updateQuery);
|
|
91
|
+
|
|
92
|
+
// Replace old column
|
|
93
|
+
await context.removeColumn(tableName, columnName);
|
|
94
|
+
await context.renameColumn(tableName, tempColumnName, columnName);
|
|
95
|
+
|
|
96
|
+
await context.changeColumn(tableName, columnName, {
|
|
97
|
+
type: DataTypes.DATE,
|
|
98
|
+
allowNull: options.allowNull,
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Define columns to rollback (reverse order)
|
|
103
|
+
const rollbacks = [
|
|
104
|
+
{ table: 'discounts', column: 'end', allowNull: true },
|
|
105
|
+
{ table: 'discounts', column: 'start', allowNull: false, convertAllRows: true },
|
|
106
|
+
{ table: 'coupons', column: 'redeem_by', allowNull: true },
|
|
107
|
+
{ table: 'promotion_codes', column: 'expires_at', allowNull: true },
|
|
108
|
+
];
|
|
109
|
+
for (const rollback of rollbacks) {
|
|
110
|
+
// eslint-disable-next-line no-await-in-loop
|
|
111
|
+
await migrateIntegerToDate(rollback.table, rollback.column, {
|
|
112
|
+
allowNull: rollback.allowNull,
|
|
113
|
+
convertAllRows: rollback.convertAllRows,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { Migration, safeApplyColumnChanges } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
await safeApplyColumnChanges(context, {
|
|
6
|
+
coupons: [
|
|
7
|
+
{
|
|
8
|
+
name: 'description',
|
|
9
|
+
field: {
|
|
10
|
+
type: DataTypes.TEXT,
|
|
11
|
+
allowNull: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
promotion_codes: [
|
|
16
|
+
{
|
|
17
|
+
name: 'description',
|
|
18
|
+
field: {
|
|
19
|
+
type: DataTypes.TEXT,
|
|
20
|
+
allowNull: true,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const down: Migration = async ({ context }) => {
|
|
28
|
+
await context.removeColumn('coupons', 'description');
|
|
29
|
+
await context.removeColumn('promotion_codes', 'description');
|
|
30
|
+
};
|
|
@@ -196,6 +196,14 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
|
|
|
196
196
|
|
|
197
197
|
declare cross_sell_behavior?: LiteralUnion<'auto' | 'required', string>;
|
|
198
198
|
|
|
199
|
+
// FIXME: Only exist on creation
|
|
200
|
+
declare discounts?: Array<{
|
|
201
|
+
coupon?: string;
|
|
202
|
+
promotion_code?: string;
|
|
203
|
+
discount_amount?: string;
|
|
204
|
+
verification_method?: string;
|
|
205
|
+
verification_data?: Record<string, any>;
|
|
206
|
+
}>;
|
|
199
207
|
declare fulfillment_status?: LiteralUnion<
|
|
200
208
|
| 'pending'
|
|
201
209
|
| 'processing'
|
|
@@ -425,6 +433,10 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
|
|
|
425
433
|
this.init(
|
|
426
434
|
{
|
|
427
435
|
...CheckoutSession.GENESIS_ATTRIBUTES,
|
|
436
|
+
discounts: {
|
|
437
|
+
type: DataTypes.JSON,
|
|
438
|
+
allowNull: true,
|
|
439
|
+
},
|
|
428
440
|
payment_intent_data: {
|
|
429
441
|
type: DataTypes.JSON,
|
|
430
442
|
allowNull: true,
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/lines-between-class-members */
|
|
2
2
|
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
|
|
3
3
|
import type { LiteralUnion } from 'type-fest';
|
|
4
|
+
import { fromTokenToUnit } from '@ocap/util';
|
|
4
5
|
|
|
5
|
-
import {
|
|
6
|
+
import { createEvent } from '../../libs/audit';
|
|
7
|
+
import { createCodeGenerator, formatMetadata } from '../../libs/util';
|
|
8
|
+
import { trimDecimals } from '../../libs/math-utils';
|
|
9
|
+
import logger from '../../libs/logger';
|
|
10
|
+
import type { TPaymentCurrency } from './payment-currency';
|
|
6
11
|
|
|
7
12
|
const nextId = createCodeGenerator('', 8);
|
|
8
13
|
|
|
@@ -10,6 +15,7 @@ const nextId = createCodeGenerator('', 8);
|
|
|
10
15
|
export class Coupon extends Model<InferAttributes<Coupon>, InferCreationAttributes<Coupon>> {
|
|
11
16
|
declare id: CreationOptional<string>;
|
|
12
17
|
declare livemode: boolean;
|
|
18
|
+
declare locked: CreationOptional<boolean>;
|
|
13
19
|
|
|
14
20
|
declare amount_off: string;
|
|
15
21
|
declare percent_off: number;
|
|
@@ -22,23 +28,32 @@ export class Coupon extends Model<InferAttributes<Coupon>, InferCreationAttribut
|
|
|
22
28
|
|
|
23
29
|
declare name: string;
|
|
24
30
|
|
|
31
|
+
declare description?: string;
|
|
32
|
+
|
|
25
33
|
declare metadata: Record<string, any>;
|
|
26
34
|
|
|
27
35
|
declare applies_to?: {
|
|
28
36
|
products: string[];
|
|
29
37
|
};
|
|
30
38
|
|
|
31
|
-
declare currency_options?: Record<
|
|
39
|
+
declare currency_options?: Record<
|
|
40
|
+
string,
|
|
41
|
+
{
|
|
42
|
+
amount_off: string;
|
|
43
|
+
}
|
|
44
|
+
>;
|
|
32
45
|
|
|
33
46
|
declare max_redemptions?: number;
|
|
34
47
|
|
|
35
|
-
declare redeem_by?:
|
|
48
|
+
declare redeem_by?: number;
|
|
36
49
|
|
|
37
50
|
declare times_redeemed?: number;
|
|
38
51
|
|
|
39
52
|
declare valid?: boolean;
|
|
53
|
+
declare created_via: LiteralUnion<'api' | 'dashboard' | 'portal', string>;
|
|
40
54
|
|
|
41
55
|
declare created_at: CreationOptional<Date>;
|
|
56
|
+
|
|
42
57
|
declare updated_at: CreationOptional<Date>;
|
|
43
58
|
|
|
44
59
|
public static readonly GENESIS_ATTRIBUTES = {
|
|
@@ -52,6 +67,10 @@ export class Coupon extends Model<InferAttributes<Coupon>, InferCreationAttribut
|
|
|
52
67
|
type: DataTypes.BOOLEAN,
|
|
53
68
|
allowNull: false,
|
|
54
69
|
},
|
|
70
|
+
locked: {
|
|
71
|
+
type: DataTypes.BOOLEAN,
|
|
72
|
+
defaultValue: false,
|
|
73
|
+
},
|
|
55
74
|
amount_off: {
|
|
56
75
|
type: DataTypes.STRING(32),
|
|
57
76
|
defaultValue: '0',
|
|
@@ -63,18 +82,25 @@ export class Coupon extends Model<InferAttributes<Coupon>, InferCreationAttribut
|
|
|
63
82
|
currency_id: {
|
|
64
83
|
type: DataTypes.STRING(15),
|
|
65
84
|
allowNull: false,
|
|
85
|
+
defaultValue: '',
|
|
66
86
|
},
|
|
67
87
|
duration: {
|
|
68
88
|
type: DataTypes.ENUM('forever', 'once', 'repeating'),
|
|
89
|
+
allowNull: false,
|
|
69
90
|
},
|
|
70
91
|
duration_in_months: {
|
|
71
92
|
type: DataTypes.INTEGER,
|
|
72
93
|
allowNull: false,
|
|
94
|
+
defaultValue: 0,
|
|
73
95
|
},
|
|
74
96
|
name: {
|
|
75
97
|
type: DataTypes.STRING(64),
|
|
76
98
|
allowNull: false,
|
|
77
99
|
},
|
|
100
|
+
description: {
|
|
101
|
+
type: DataTypes.TEXT,
|
|
102
|
+
allowNull: true,
|
|
103
|
+
},
|
|
78
104
|
applies_to: {
|
|
79
105
|
type: DataTypes.JSON,
|
|
80
106
|
allowNull: true,
|
|
@@ -88,7 +114,7 @@ export class Coupon extends Model<InferAttributes<Coupon>, InferCreationAttribut
|
|
|
88
114
|
allowNull: true,
|
|
89
115
|
},
|
|
90
116
|
redeem_by: {
|
|
91
|
-
type: DataTypes.
|
|
117
|
+
type: DataTypes.INTEGER,
|
|
92
118
|
allowNull: true,
|
|
93
119
|
},
|
|
94
120
|
times_redeemed: {
|
|
@@ -103,6 +129,10 @@ export class Coupon extends Model<InferAttributes<Coupon>, InferCreationAttribut
|
|
|
103
129
|
type: DataTypes.JSON,
|
|
104
130
|
allowNull: true,
|
|
105
131
|
},
|
|
132
|
+
created_via: {
|
|
133
|
+
type: DataTypes.ENUM('api', 'dashboard', 'portal'),
|
|
134
|
+
allowNull: false,
|
|
135
|
+
},
|
|
106
136
|
created_at: {
|
|
107
137
|
type: DataTypes.DATE,
|
|
108
138
|
defaultValue: DataTypes.NOW,
|
|
@@ -122,6 +152,14 @@ export class Coupon extends Model<InferAttributes<Coupon>, InferCreationAttribut
|
|
|
122
152
|
tableName: 'coupons',
|
|
123
153
|
createdAt: 'created_at',
|
|
124
154
|
updatedAt: 'updated_at',
|
|
155
|
+
hooks: {
|
|
156
|
+
afterCreate: (model: Coupon, options) =>
|
|
157
|
+
createEvent('Coupon', 'coupon.created', model, options).catch(console.error),
|
|
158
|
+
afterUpdate: (model: Coupon, options) =>
|
|
159
|
+
createEvent('Coupon', 'coupon.updated', model, options).catch(console.error),
|
|
160
|
+
afterDestroy: (model: Coupon, options) =>
|
|
161
|
+
createEvent('Coupon', 'coupon.deleted', model, options).catch(console.error),
|
|
162
|
+
},
|
|
125
163
|
});
|
|
126
164
|
}
|
|
127
165
|
|
|
@@ -132,6 +170,108 @@ export class Coupon extends Model<InferAttributes<Coupon>, InferCreationAttribut
|
|
|
132
170
|
as: 'currency',
|
|
133
171
|
});
|
|
134
172
|
}
|
|
173
|
+
|
|
174
|
+
public static formatBeforeSave(coupon: Partial<TCoupon>) {
|
|
175
|
+
if (coupon.metadata) {
|
|
176
|
+
coupon.metadata = formatMetadata(coupon.metadata);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (coupon.max_redemptions) {
|
|
180
|
+
coupon.max_redemptions = Number(coupon.max_redemptions);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (coupon.times_redeemed) {
|
|
184
|
+
coupon.times_redeemed = Number(coupon.times_redeemed);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (coupon.duration === 'repeating') {
|
|
188
|
+
coupon.duration_in_months = Number(coupon.duration_in_months || 0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return coupon;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public static async insert(coupon: Partial<TCoupon>) {
|
|
195
|
+
const formattedData = this.formatBeforeSave({
|
|
196
|
+
locked: false,
|
|
197
|
+
times_redeemed: 0,
|
|
198
|
+
valid: true,
|
|
199
|
+
metadata: {},
|
|
200
|
+
amount_off: '0',
|
|
201
|
+
currency_options: {},
|
|
202
|
+
currency_id: '',
|
|
203
|
+
duration_in_months: 0,
|
|
204
|
+
...coupon,
|
|
205
|
+
});
|
|
206
|
+
logger.info('Formatted data', { formattedData });
|
|
207
|
+
try {
|
|
208
|
+
return await this.create(formattedData as any);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
logger.error('Coupon creation error details:', {
|
|
211
|
+
error: error.message,
|
|
212
|
+
formattedData,
|
|
213
|
+
validationErrors: error.errors || [],
|
|
214
|
+
sqlMessage: error.original?.message || 'No SQL message',
|
|
215
|
+
});
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
public async isUsed() {
|
|
221
|
+
const { Discount } = this.sequelize.models;
|
|
222
|
+
const discountCount = await Discount!.count({
|
|
223
|
+
where: { coupon_id: this.id },
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (discountCount > 0) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
public async getAppliedProducts() {
|
|
234
|
+
if (!this.applies_to || !this.applies_to?.products) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
const { Product } = this.sequelize.models;
|
|
238
|
+
const productsWithPrices = await Promise.all(
|
|
239
|
+
// @ts-ignore
|
|
240
|
+
this.applies_to.products.map((productId) => Product!.expand(productId))
|
|
241
|
+
);
|
|
242
|
+
return productsWithPrices;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Format currency options for storage - convert unit amounts to token amounts
|
|
247
|
+
*/
|
|
248
|
+
public static formatCurrencyOptions(
|
|
249
|
+
currencyOptions: Record<string, { amount_off: string }>,
|
|
250
|
+
currencies: TPaymentCurrency[]
|
|
251
|
+
): Record<
|
|
252
|
+
string,
|
|
253
|
+
{
|
|
254
|
+
amount_off: string;
|
|
255
|
+
}
|
|
256
|
+
> {
|
|
257
|
+
const formatted: Record<string, { amount_off: string }> = {};
|
|
258
|
+
|
|
259
|
+
for (const [currencyId, amountInfo] of Object.entries(currencyOptions || {})) {
|
|
260
|
+
const currency = currencies.find((c) => c.id === currencyId);
|
|
261
|
+
if (!currency) {
|
|
262
|
+
throw new Error(`currency ${currencyId} used in coupon not found or inactive`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
formatted[currencyId] = {
|
|
266
|
+
amount_off: fromTokenToUnit(
|
|
267
|
+
trimDecimals(amountInfo.amount_off || '0', currency.decimal),
|
|
268
|
+
currency.decimal
|
|
269
|
+
).toString(),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return formatted;
|
|
274
|
+
}
|
|
135
275
|
}
|
|
136
276
|
|
|
137
277
|
export type TCoupon = InferAttributes<Coupon>;
|
|
@@ -20,7 +20,11 @@ export class Discount extends Model<InferAttributes<Discount>, InferCreationAttr
|
|
|
20
20
|
declare invoice_item_id?: string;
|
|
21
21
|
|
|
22
22
|
declare start: number;
|
|
23
|
-
declare end: number;
|
|
23
|
+
declare end: number | null;
|
|
24
|
+
|
|
25
|
+
declare verification_method?: string;
|
|
26
|
+
declare verification_data?: Record<string, any>;
|
|
27
|
+
declare metadata?: Record<string, any>;
|
|
24
28
|
|
|
25
29
|
declare created_at: CreationOptional<Date>;
|
|
26
30
|
declare updated_at: CreationOptional<Date>;
|
|
@@ -65,11 +69,19 @@ export class Discount extends Model<InferAttributes<Discount>, InferCreationAttr
|
|
|
65
69
|
allowNull: true,
|
|
66
70
|
},
|
|
67
71
|
start: {
|
|
68
|
-
type: DataTypes.
|
|
72
|
+
type: DataTypes.INTEGER,
|
|
69
73
|
allowNull: false,
|
|
70
74
|
},
|
|
71
75
|
end: {
|
|
72
|
-
type: DataTypes.
|
|
76
|
+
type: DataTypes.INTEGER,
|
|
77
|
+
allowNull: true,
|
|
78
|
+
},
|
|
79
|
+
verification_method: {
|
|
80
|
+
type: DataTypes.STRING(50),
|
|
81
|
+
allowNull: true,
|
|
82
|
+
},
|
|
83
|
+
verification_data: {
|
|
84
|
+
type: DataTypes.JSON,
|
|
73
85
|
allowNull: true,
|
|
74
86
|
},
|
|
75
87
|
metadata: {
|
|
@@ -98,18 +110,19 @@ export class Discount extends Model<InferAttributes<Discount>, InferCreationAttr
|
|
|
98
110
|
});
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
// FIXME:
|
|
102
113
|
public static associate(models: any) {
|
|
103
|
-
this.
|
|
104
|
-
|
|
105
|
-
foreignKey: 'id',
|
|
114
|
+
this.belongsTo(models.Customer, {
|
|
115
|
+
foreignKey: 'customer_id',
|
|
106
116
|
as: 'customer',
|
|
107
117
|
});
|
|
108
|
-
this.
|
|
109
|
-
|
|
110
|
-
foreignKey: 'id',
|
|
118
|
+
this.belongsTo(models.Coupon, {
|
|
119
|
+
foreignKey: 'coupon_id',
|
|
111
120
|
as: 'coupon',
|
|
112
121
|
});
|
|
122
|
+
this.belongsTo(models.PromotionCode, {
|
|
123
|
+
foreignKey: 'promotion_code_id',
|
|
124
|
+
as: 'promotionCode',
|
|
125
|
+
});
|
|
113
126
|
}
|
|
114
127
|
}
|
|
115
128
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CheckoutSession, TCheckoutSession } from './checkout-session';
|
|
2
|
-
import { Coupon } from './coupon';
|
|
2
|
+
import { Coupon, TCoupon } from './coupon';
|
|
3
3
|
import { Customer, TCustomer } from './customer';
|
|
4
4
|
import { Discount } from './discount';
|
|
5
5
|
import { Event, TEvent } from './event';
|
|
@@ -16,7 +16,7 @@ import { Payout, TPayout } from './payout';
|
|
|
16
16
|
import { Price, TPrice } from './price';
|
|
17
17
|
import { PricingTable, TPricingTable } from './pricing-table';
|
|
18
18
|
import { Product, TProduct } from './product';
|
|
19
|
-
import { PromotionCode } from './promotion-code';
|
|
19
|
+
import { PromotionCode, TPromotionCode } from './promotion-code';
|
|
20
20
|
import { Refund, TRefund } from './refund';
|
|
21
21
|
import { SetupIntent, TSetupIntent } from './setup-intent';
|
|
22
22
|
import { Subscription, TSubscription } from './subscription';
|
|
@@ -321,3 +321,14 @@ export type CreditGrantSummary = {
|
|
|
321
321
|
remainingAmount: string;
|
|
322
322
|
grantCount: number;
|
|
323
323
|
};
|
|
324
|
+
|
|
325
|
+
export type TCouponExpanded = TCoupon & {
|
|
326
|
+
object: 'coupon';
|
|
327
|
+
applied_products?: TProduct[];
|
|
328
|
+
promotion_codes?: TPromotionCode[];
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export type TPromotionCodeExpanded = TPromotionCode & {
|
|
332
|
+
object: 'promotion_code';
|
|
333
|
+
coupon?: TCoupon;
|
|
334
|
+
};
|