payment-kit 1.21.12 → 1.21.14
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/payment-stat.ts +31 -23
- package/api/src/libs/invoice.ts +29 -4
- package/api/src/libs/product.ts +28 -4
- package/api/src/routes/checkout-sessions.ts +46 -1
- package/api/src/routes/connect/re-stake.ts +2 -0
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/invoices.ts +63 -2
- package/api/src/routes/payment-stats.ts +244 -22
- package/api/src/routes/products.ts +3 -0
- package/api/src/routes/subscriptions.ts +2 -1
- package/api/src/routes/tax-rates.ts +220 -0
- package/api/src/store/migrations/20251001-add-tax-code-to-products.ts +20 -0
- package/api/src/store/migrations/20251001-create-tax-rates.ts +17 -0
- package/api/src/store/migrations/20251007-relate-tax-rate-to-invoice.ts +24 -0
- package/api/src/store/migrations/20251009-add-tax-behavior.ts +21 -0
- package/api/src/store/models/index.ts +3 -0
- package/api/src/store/models/invoice-item.ts +10 -0
- package/api/src/store/models/price.ts +7 -0
- package/api/src/store/models/product.ts +7 -0
- package/api/src/store/models/tax-rate.ts +352 -0
- package/api/tests/models/tax-rate.spec.ts +777 -0
- package/blocklet.yml +2 -2
- package/package.json +6 -6
- package/public/currencies/dollar.png +0 -0
- package/src/components/collapse.tsx +3 -2
- package/src/components/drawer-form.tsx +2 -1
- package/src/components/invoice/list.tsx +38 -1
- package/src/components/invoice/table.tsx +48 -2
- package/src/components/metadata/form.tsx +2 -2
- package/src/components/payment-intent/list.tsx +19 -1
- package/src/components/payouts/list.tsx +19 -1
- package/src/components/price/currency-select.tsx +105 -48
- package/src/components/price/form.tsx +3 -1
- package/src/components/product/form.tsx +79 -5
- package/src/components/refund/list.tsx +20 -1
- package/src/components/subscription/items/actions.tsx +25 -15
- package/src/components/subscription/list.tsx +16 -1
- package/src/components/tax/actions.tsx +140 -0
- package/src/components/tax/filter-toolbar.tsx +230 -0
- package/src/components/tax/tax-code-select.tsx +633 -0
- package/src/components/tax/tax-rate-form.tsx +177 -0
- package/src/components/tax/tax-utils.ts +38 -0
- package/src/components/tax/taxCodes.json +10882 -0
- package/src/components/uploader.tsx +3 -0
- package/src/locales/en.tsx +152 -0
- package/src/locales/zh.tsx +149 -0
- package/src/pages/admin/billing/invoices/detail.tsx +1 -1
- package/src/pages/admin/index.tsx +2 -0
- package/src/pages/admin/overview.tsx +1114 -322
- package/src/pages/admin/products/vendors/index.tsx +4 -2
- package/src/pages/admin/tax/create.tsx +104 -0
- package/src/pages/admin/tax/detail.tsx +476 -0
- package/src/pages/admin/tax/edit.tsx +126 -0
- package/src/pages/admin/tax/index.tsx +86 -0
- package/src/pages/admin/tax/list.tsx +334 -0
- package/src/pages/customer/subscription/change-payment.tsx +1 -1
- package/src/pages/home.tsx +6 -3
|
@@ -37,6 +37,7 @@ const ProductAndPriceSchema = Joi.object({
|
|
|
37
37
|
description: Joi.string().max(250).empty('').optional(),
|
|
38
38
|
images: Joi.any().optional(),
|
|
39
39
|
metadata: MetadataSchema,
|
|
40
|
+
tax_code: Joi.string().max(30).empty('').optional(),
|
|
40
41
|
statement_descriptor: Joi.string()
|
|
41
42
|
.max(22)
|
|
42
43
|
.pattern(/^(?=.*[A-Za-z])[^\u4e00-\u9fa5<>"’\\]*$/)
|
|
@@ -92,6 +93,7 @@ export async function createProductAndPrices(payload: any) {
|
|
|
92
93
|
'nft_factory',
|
|
93
94
|
'features',
|
|
94
95
|
'metadata',
|
|
96
|
+
'tax_code',
|
|
95
97
|
]);
|
|
96
98
|
|
|
97
99
|
if (Array.isArray(payload.vendor_config) && payload.vendor_config.length > 0) {
|
|
@@ -431,6 +433,7 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
431
433
|
'nft_factory',
|
|
432
434
|
'metadata',
|
|
433
435
|
'cross_sell',
|
|
436
|
+
'tax_code',
|
|
434
437
|
]);
|
|
435
438
|
|
|
436
439
|
if (Array.isArray(req.body.vendor_config)) {
|
|
@@ -538,7 +538,8 @@ router.put('/:id/recover', authPortal, async (req, res) => {
|
|
|
538
538
|
cancel_at: 0,
|
|
539
539
|
};
|
|
540
540
|
|
|
541
|
-
await updateStripeSubscription(doc, {
|
|
541
|
+
await updateStripeSubscription(doc, { cancel_at_period_end: false });
|
|
542
|
+
await updateStripeSubscription(doc, { cancel_at: null });
|
|
542
543
|
|
|
543
544
|
// @ts-ignore
|
|
544
545
|
await doc.update({
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import Joi from 'joi';
|
|
3
|
+
|
|
4
|
+
import { CustomError } from '@blocklet/error';
|
|
5
|
+
import { literal } from 'sequelize';
|
|
6
|
+
import { authenticate } from '../libs/security';
|
|
7
|
+
import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
|
|
8
|
+
import { formatMetadata } from '../libs/util';
|
|
9
|
+
import { InvoiceItem, TaxRate } from '../store/models';
|
|
10
|
+
|
|
11
|
+
const router = Router();
|
|
12
|
+
const auth = authenticate({ component: true, roles: ['owner', 'admin'] });
|
|
13
|
+
|
|
14
|
+
const createTaxRateSchema = Joi.object({
|
|
15
|
+
country: Joi.string().length(2).uppercase().required(),
|
|
16
|
+
state: Joi.string().max(50).empty('').optional(),
|
|
17
|
+
postal_code: Joi.string().max(20).empty('').optional(),
|
|
18
|
+
tax_code: Joi.string().max(20).empty('').optional(),
|
|
19
|
+
percentage: Joi.number().min(0).max(99.9999).required(),
|
|
20
|
+
display_name: Joi.string().max(100).required(),
|
|
21
|
+
description: Joi.string().max(500).empty('').optional(),
|
|
22
|
+
active: Joi.boolean().default(true),
|
|
23
|
+
metadata: MetadataSchema,
|
|
24
|
+
}).unknown(true);
|
|
25
|
+
|
|
26
|
+
const updateTaxRateSchema = Joi.object({
|
|
27
|
+
active: Joi.boolean().optional(),
|
|
28
|
+
percentage: Joi.number().min(0).max(99.9999).optional(),
|
|
29
|
+
display_name: Joi.string().max(100).optional(),
|
|
30
|
+
description: Joi.string().max(500).empty('').optional(),
|
|
31
|
+
metadata: MetadataSchema,
|
|
32
|
+
}).unknown(true);
|
|
33
|
+
|
|
34
|
+
const listSchema = createListParamSchema<{
|
|
35
|
+
country?: string;
|
|
36
|
+
state?: string;
|
|
37
|
+
active?: boolean;
|
|
38
|
+
}>({
|
|
39
|
+
country: Joi.string().length(2).uppercase().optional(),
|
|
40
|
+
state: Joi.string().max(50).optional(),
|
|
41
|
+
active: Joi.boolean().optional(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// List tax rates
|
|
45
|
+
router.get('/', auth, async (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const { error, value: validated } = listSchema.validate(req.query, { stripUnknown: true });
|
|
48
|
+
if (error) {
|
|
49
|
+
res.status(400).json({ error: `Validation error: ${error.message}` });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { page, pageSize, ...query } = validated;
|
|
54
|
+
const where = getWhereFromKvQuery(query.q);
|
|
55
|
+
|
|
56
|
+
if (typeof req.livemode === 'boolean') {
|
|
57
|
+
where.livemode = req.livemode;
|
|
58
|
+
}
|
|
59
|
+
if (query.country) {
|
|
60
|
+
where.country = query.country;
|
|
61
|
+
}
|
|
62
|
+
if (query.state) {
|
|
63
|
+
where.state = query.state;
|
|
64
|
+
}
|
|
65
|
+
if (typeof query.active === 'boolean') {
|
|
66
|
+
where.active = query.active;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { rows: list, count } = await TaxRate.findAndCountAll({
|
|
70
|
+
where,
|
|
71
|
+
order: getOrder(req.query, [['created_at', 'DESC']]),
|
|
72
|
+
offset: (page - 1) * pageSize,
|
|
73
|
+
limit: pageSize,
|
|
74
|
+
attributes: {
|
|
75
|
+
include: [
|
|
76
|
+
[
|
|
77
|
+
literal(`(
|
|
78
|
+
SELECT COUNT(DISTINCT ii.invoice_id)
|
|
79
|
+
FROM invoice_items AS ii
|
|
80
|
+
WHERE ii.tax_rate_id = TaxRate.id
|
|
81
|
+
)`),
|
|
82
|
+
'invoice_count',
|
|
83
|
+
],
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
res.json({ count, list });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
res.status(400).json({ error: error?.message });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Get a specific tax rate
|
|
95
|
+
router.get('/:id', auth, async (req, res) => {
|
|
96
|
+
const taxRate = await TaxRate.findOne({
|
|
97
|
+
where: {
|
|
98
|
+
id: req.params.id,
|
|
99
|
+
livemode: req.livemode,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!taxRate) {
|
|
104
|
+
throw new CustomError(404, 'Tax rate not found');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
res.json(taxRate);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Create a new tax rate
|
|
111
|
+
router.post('/', auth, async (req, res) => {
|
|
112
|
+
const { error, value: data } = createTaxRateSchema.validate(req.body, { stripUnknown: true });
|
|
113
|
+
if (error) {
|
|
114
|
+
res.status(400).json({ error: `Validation failed: ${error.message}` });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const existingRate = await TaxRate.findOne({
|
|
119
|
+
where: {
|
|
120
|
+
livemode: req.livemode,
|
|
121
|
+
country: data.country,
|
|
122
|
+
state: data.state || null,
|
|
123
|
+
postal_code: data.postal_code || null,
|
|
124
|
+
tax_code: data.tax_code || null,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (existingRate) {
|
|
129
|
+
throw new CustomError(
|
|
130
|
+
400,
|
|
131
|
+
`Tax rate ${existingRate.id} already exists, you can create a new tax rate with different region or update the existing tax rate`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const taxRate = await TaxRate.create({
|
|
136
|
+
...data,
|
|
137
|
+
livemode: req.livemode,
|
|
138
|
+
metadata: formatMetadata(data.metadata),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
res.json(taxRate);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Update a tax rate
|
|
145
|
+
router.put('/:id', auth, async (req, res) => {
|
|
146
|
+
const { error, value: data } = updateTaxRateSchema.validate(req.body, { stripUnknown: true });
|
|
147
|
+
if (error) {
|
|
148
|
+
res.status(400).json({ error: `Validation failed: ${error.message}` });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const taxRate = await TaxRate.findOne({
|
|
153
|
+
where: {
|
|
154
|
+
id: req.params.id,
|
|
155
|
+
livemode: req.livemode,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!taxRate) {
|
|
160
|
+
throw new CustomError(404, 'Tax rate not found');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await taxRate.update({
|
|
164
|
+
...data,
|
|
165
|
+
metadata: data.metadata ? formatMetadata(data.metadata) : taxRate.metadata,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
res.json(taxRate);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Delete a tax rate
|
|
172
|
+
router.delete('/:id', auth, async (req, res) => {
|
|
173
|
+
const taxRate = await TaxRate.findOne({
|
|
174
|
+
where: {
|
|
175
|
+
id: req.params.id,
|
|
176
|
+
livemode: req.livemode,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (!taxRate) {
|
|
181
|
+
throw new CustomError(404, 'Tax rate not found');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const existInvoice = await InvoiceItem.findOne({ where: { tax_rate_id: taxRate.id } });
|
|
185
|
+
if (existInvoice) {
|
|
186
|
+
throw new CustomError(400, 'Tax rate is used in an invoice, you can not delete it');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await taxRate.destroy();
|
|
190
|
+
|
|
191
|
+
res.json({ deleted: true, id: req.params.id });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Find matching tax rate for a given location and tax code
|
|
195
|
+
router.post('/match', auth, async (req, res) => {
|
|
196
|
+
const schema = Joi.object({
|
|
197
|
+
country: Joi.string().length(2).uppercase().required(),
|
|
198
|
+
state: Joi.string().max(50).empty('').optional(),
|
|
199
|
+
postal_code: Joi.string().max(20).empty('').optional(),
|
|
200
|
+
tax_code: Joi.string().max(20).empty('').optional(),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const { error, value: data } = schema.validate(req.body, { stripUnknown: true });
|
|
204
|
+
if (error) {
|
|
205
|
+
res.status(400).json({ error: error.message });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const matchedRate = await TaxRate.findMatchingRate({
|
|
210
|
+
country: data.country,
|
|
211
|
+
state: data.state,
|
|
212
|
+
postalCode: data.postal_code,
|
|
213
|
+
taxCode: data.tax_code,
|
|
214
|
+
livemode: req.livemode,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
res.json(matchedRate);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
export default router;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { safeApplyColumnChanges, type Migration } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
await safeApplyColumnChanges(context, {
|
|
6
|
+
products: [
|
|
7
|
+
{
|
|
8
|
+
name: 'tax_code',
|
|
9
|
+
field: {
|
|
10
|
+
type: DataTypes.STRING(20),
|
|
11
|
+
allowNull: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const down: Migration = async ({ context }) => {
|
|
19
|
+
await context.removeColumn('products', 'tax_code');
|
|
20
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createIndexIfNotExists, type Migration } from '../migrate';
|
|
2
|
+
import models from '../models';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
await context.createTable('tax_rates', models.TaxRate.GENESIS_ATTRIBUTES);
|
|
6
|
+
|
|
7
|
+
await createIndexIfNotExists(
|
|
8
|
+
context,
|
|
9
|
+
'tax_rates',
|
|
10
|
+
['country', 'state', 'postal_code', 'tax_code'],
|
|
11
|
+
'tax_rates_query_index'
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const down: Migration = async ({ context }) => {
|
|
16
|
+
await context.dropTable('tax_rates');
|
|
17
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { createIndexIfNotExists, safeApplyColumnChanges, type Migration } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
await safeApplyColumnChanges(context, {
|
|
6
|
+
invoice_items: [
|
|
7
|
+
{
|
|
8
|
+
name: 'tax_rate_id',
|
|
9
|
+
field: {
|
|
10
|
+
type: DataTypes.STRING(30),
|
|
11
|
+
allowNull: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
await createIndexIfNotExists(context, 'invoice_items', ['tax_rate_id'], 'idx_invoice_items_tax_rate_id');
|
|
17
|
+
await createIndexIfNotExists(context, 'invoice_items', ['invoice_id'], 'idx_invoice_items_invoice_id');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const down: Migration = async ({ context }) => {
|
|
21
|
+
await context.removeColumn('invoice_items', 'tax_rate_id');
|
|
22
|
+
await context.removeIndex('invoice_items', 'idx_invoice_items_tax_rate_id');
|
|
23
|
+
await context.removeIndex('invoice_items', 'idx_invoice_items_invoice_id');
|
|
24
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { safeApplyColumnChanges, type Migration } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
await safeApplyColumnChanges(context, {
|
|
6
|
+
prices: [
|
|
7
|
+
{
|
|
8
|
+
name: 'tax_behavior',
|
|
9
|
+
field: {
|
|
10
|
+
type: DataTypes.ENUM('inclusive', 'exclusive'),
|
|
11
|
+
allowNull: false,
|
|
12
|
+
defaultValue: 'inclusive',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const down: Migration = async ({ context }) => {
|
|
20
|
+
await context.removeColumn('prices', 'tax_behavior');
|
|
21
|
+
};
|
|
@@ -33,6 +33,7 @@ import { Meter, TMeter } from './meter';
|
|
|
33
33
|
import { MeterEvent, TMeterEvent } from './meter-event';
|
|
34
34
|
import { AutoRechargeConfig } from './auto-recharge-config';
|
|
35
35
|
import { ProductVendor } from './product-vendor';
|
|
36
|
+
import { TaxRate } from './tax-rate';
|
|
36
37
|
|
|
37
38
|
const models = {
|
|
38
39
|
CheckoutSession,
|
|
@@ -69,6 +70,7 @@ const models = {
|
|
|
69
70
|
MeterEvent,
|
|
70
71
|
AutoRechargeConfig,
|
|
71
72
|
ProductVendor,
|
|
73
|
+
TaxRate,
|
|
72
74
|
};
|
|
73
75
|
|
|
74
76
|
export function initialize(sequelize: any) {
|
|
@@ -119,6 +121,7 @@ export * from './meter';
|
|
|
119
121
|
export * from './meter-event';
|
|
120
122
|
export * from './auto-recharge-config';
|
|
121
123
|
export * from './product-vendor';
|
|
124
|
+
export * from './tax-rate';
|
|
122
125
|
|
|
123
126
|
export type TPriceExpanded = TPrice & {
|
|
124
127
|
object: 'price';
|
|
@@ -32,6 +32,7 @@ export class InvoiceItem extends Model<InferAttributes<InvoiceItem>, InferCreati
|
|
|
32
32
|
declare subscription_id?: string;
|
|
33
33
|
declare subscription_item_id?: string;
|
|
34
34
|
declare test_clock_id?: string;
|
|
35
|
+
declare tax_rate_id?: string;
|
|
35
36
|
|
|
36
37
|
declare discountable: boolean;
|
|
37
38
|
declare discount_amounts: DiscountAmount[];
|
|
@@ -106,6 +107,10 @@ export class InvoiceItem extends Model<InferAttributes<InvoiceItem>, InferCreati
|
|
|
106
107
|
type: DataTypes.STRING(30),
|
|
107
108
|
allowNull: true,
|
|
108
109
|
},
|
|
110
|
+
tax_rate_id: {
|
|
111
|
+
type: DataTypes.STRING(30),
|
|
112
|
+
allowNull: true,
|
|
113
|
+
},
|
|
109
114
|
discountable: {
|
|
110
115
|
type: DataTypes.BOOLEAN,
|
|
111
116
|
allowNull: false,
|
|
@@ -184,6 +189,11 @@ export class InvoiceItem extends Model<InferAttributes<InvoiceItem>, InferCreati
|
|
|
184
189
|
foreignKey: 'id',
|
|
185
190
|
as: 'subscriptionItem',
|
|
186
191
|
});
|
|
192
|
+
this.hasOne(models.TaxRate, {
|
|
193
|
+
sourceKey: 'tax_rate_id',
|
|
194
|
+
foreignKey: 'id',
|
|
195
|
+
as: 'tax_rate',
|
|
196
|
+
});
|
|
187
197
|
}
|
|
188
198
|
|
|
189
199
|
public static async isPriceUsed(priceId: string) {
|
|
@@ -99,6 +99,8 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
99
99
|
// Quantity limit per checkout, 0 means no limit
|
|
100
100
|
declare quantity_limit_per_checkout: number;
|
|
101
101
|
|
|
102
|
+
declare tax_behavior?: LiteralUnion<'inclusive' | 'exclusive', string>;
|
|
103
|
+
|
|
102
104
|
public static readonly GENESIS_ATTRIBUTES = {
|
|
103
105
|
id: {
|
|
104
106
|
type: DataTypes.STRING(32),
|
|
@@ -211,6 +213,11 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
211
213
|
defaultValue: 0,
|
|
212
214
|
allowNull: false,
|
|
213
215
|
},
|
|
216
|
+
tax_behavior: {
|
|
217
|
+
type: DataTypes.ENUM('inclusive', 'exclusive'),
|
|
218
|
+
allowNull: false,
|
|
219
|
+
defaultValue: 'inclusive',
|
|
220
|
+
},
|
|
214
221
|
},
|
|
215
222
|
{
|
|
216
223
|
sequelize,
|
|
@@ -47,6 +47,9 @@ export class Product extends Model<InferAttributes<Product>, InferCreationAttrib
|
|
|
47
47
|
// If set, we will mint an nft to consumer on purchase
|
|
48
48
|
declare nft_factory?: string;
|
|
49
49
|
|
|
50
|
+
// Tax code for this product (e.g., txcd_10103000)
|
|
51
|
+
declare tax_code?: string;
|
|
52
|
+
|
|
50
53
|
declare cross_sell?: {
|
|
51
54
|
cross_sells_to_id: string;
|
|
52
55
|
};
|
|
@@ -128,6 +131,10 @@ export class Product extends Model<InferAttributes<Product>, InferCreationAttrib
|
|
|
128
131
|
type: DataTypes.STRING(40),
|
|
129
132
|
allowNull: true,
|
|
130
133
|
},
|
|
134
|
+
tax_code: {
|
|
135
|
+
type: DataTypes.STRING(20),
|
|
136
|
+
allowNull: true,
|
|
137
|
+
},
|
|
131
138
|
created_at: {
|
|
132
139
|
type: DataTypes.DATE,
|
|
133
140
|
defaultValue: DataTypes.NOW,
|