@taskforcehq/taskforce 0.3.307 → 0.3.308
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/dist/components/views/PlanComparisonPage.d.ts +20 -15
- package/dist/components/views/PlanComparisonPage.js +214 -83
- package/dist/components/views/PlanComparisonPage.test.js +249 -14
- package/dist/components/views/PlansPage.js +15 -9
- package/dist/components/views/PlansPage.test.js +4 -2
- package/dist/core/GlobalSettingsService.js +18 -0
- package/dist/core/GlobalSettingsService.test.d.ts +1 -0
- package/dist/core/GlobalSettingsService.test.js +45 -0
- package/dist/core/PlanEntitlementService.d.ts +20 -0
- package/dist/core/PlanEntitlementService.js +215 -19
- package/dist/core/PlanFeatureCatalog.test.js +55 -0
- package/dist/core/Taskforce.d.ts +14 -0
- package/dist/core/Taskforce.js +6 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/migrations/taskSchemaMigrations.js +37 -15
- package/dist/server/routes/admin.js +146 -5
- package/dist/server/routes/authSupport.d.ts +1 -0
- package/dist/server/routes/authSupport.js +2 -1
- package/dist/server/routes/billing.d.ts +1 -0
- package/dist/server/routes/billing.js +105 -63
- package/dist/server/routes/billing.test.js +159 -11
- package/dist/server/routes.test.js +91 -10
- package/dist/ui/agent-logos/ChatGPT.png +0 -0
- package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
- package/dist/ui/agent-logos/antigravity.jpeg +0 -0
- package/dist/ui/agent-logos/claude-code.jpeg +0 -0
- package/dist/ui/agent-logos/codex.jpeg +0 -0
- package/dist/ui/agent-logos/cursor.png +0 -0
- package/dist/ui/agent-logos/openclaw.jpeg +0 -0
- package/dist/ui/agent-logos/windsurf.png +0 -0
- package/dist/ui/assets/{AgentsModule-2y82_3DO.js → AgentsModule-BS4Pi_nC.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTLZZCiQ.js → AnnotatedAttachmentWorkspace-CCckzWYZ.js} +1 -1
- package/dist/ui/assets/{ContextAttachmentManager-D2epuNnG.js → ContextAttachmentManager-CfG_ER7C.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-sVa-3Do6.js → DocumentWorkspace-BVaWPA25.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-Zods27y_.js → EntityActivityTimeline-DSj0ThaK.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-D4XseTwW.js → InitiativesModule-AxMkWtxH.js} +1 -1
- package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
- package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
- package/dist/ui/assets/{TaskContextUpload-uC5qx4Bv.js → TaskContextUpload-Dw4rTF4i.js} +1 -1
- package/dist/ui/assets/{TaskSettings-CqvPAsTv.js → TaskSettings-f2ga42_V.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-oOA9bcdK.js → WorkflowsModule-E4UL3mov.js} +1 -1
- package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
- package/dist/ui/assets/{index-CYyodXsg.css → index-BvAbqx5Q.css} +1 -1
- package/dist/ui/assets/{index-607WPo_X.js → index-MAHKvFqp.js} +4 -4
- package/dist/ui/index.html +2 -2
- package/dist/ui/og-image.png +0 -0
- package/package.json +1 -1
- package/dist/ui/assets/PlansPage-D5AcbO0L.js +0 -1
- package/dist/ui/assets/PlansPage-Dvqsz5zc.css +0 -1
- package/dist/ui/assets/documentReferences-DPZuTgHh.js +0 -1
|
@@ -4,6 +4,7 @@ const LEGACY_REMOVED_PLAN_FEATURE_KEYS = Object.freeze([
|
|
|
4
4
|
'documents.workspace_access'
|
|
5
5
|
]);
|
|
6
6
|
const LEGACY_WORKSPACES_FEATURE_KEY = 'workspace.switching';
|
|
7
|
+
const CUSTOM_PLAN_FEATURE_KEY_PREFIX = 'marketing.';
|
|
7
8
|
/**
|
|
8
9
|
* Handles all plan catalog, plan version, and account entitlement logic.
|
|
9
10
|
* Extracted from Taskforce.ts to reduce monolith size.
|
|
@@ -49,9 +50,105 @@ export class PlanEntitlementService {
|
|
|
49
50
|
return null;
|
|
50
51
|
return Math.max(0, Math.floor(numeric));
|
|
51
52
|
}
|
|
53
|
+
normalizeOptionalCatalogDescriptionVisible(raw) {
|
|
54
|
+
if (raw === null || raw === undefined || raw === '')
|
|
55
|
+
return null;
|
|
56
|
+
return raw === false || raw === 0 || raw === '0' ? false : true;
|
|
57
|
+
}
|
|
58
|
+
isBuiltInPlanFeatureKey(featureKey) {
|
|
59
|
+
return DEFAULT_PLAN_FEATURE_CATALOG.some((feature) => feature.featureKey === featureKey);
|
|
60
|
+
}
|
|
61
|
+
normalizeRequiredCatalogLabel(raw) {
|
|
62
|
+
return String(raw ?? '').trim();
|
|
63
|
+
}
|
|
64
|
+
slugifyCustomPlanFeatureLabel(label) {
|
|
65
|
+
const base = label
|
|
66
|
+
.trim()
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
69
|
+
.replace(/^_+|_+$/g, '');
|
|
70
|
+
return base || generateUuidV7().replace(/-/g, '_');
|
|
71
|
+
}
|
|
72
|
+
featureKeyExists(featureKey) {
|
|
73
|
+
if (this.isBuiltInPlanFeatureKey(featureKey))
|
|
74
|
+
return true;
|
|
75
|
+
const row = this.db.prepare(`
|
|
76
|
+
SELECT 1
|
|
77
|
+
FROM plan_feature_catalog_overrides
|
|
78
|
+
WHERE tenant_id = ? AND feature_key = ?
|
|
79
|
+
LIMIT 1
|
|
80
|
+
`).get(this.tenantId, featureKey);
|
|
81
|
+
return Boolean(row);
|
|
82
|
+
}
|
|
83
|
+
generateUniqueCustomPlanFeatureKey(label) {
|
|
84
|
+
const base = `${CUSTOM_PLAN_FEATURE_KEY_PREFIX}${this.slugifyCustomPlanFeatureLabel(label)}`;
|
|
85
|
+
let featureKey = base;
|
|
86
|
+
let suffix = 2;
|
|
87
|
+
while (this.featureKeyExists(featureKey)) {
|
|
88
|
+
featureKey = `${base}_${suffix}`;
|
|
89
|
+
suffix += 1;
|
|
90
|
+
}
|
|
91
|
+
return featureKey;
|
|
92
|
+
}
|
|
93
|
+
listCustomPlanFeatureCatalogEntries() {
|
|
94
|
+
const rows = this.db.prepare(`
|
|
95
|
+
SELECT feature_key, label, description, public_label, public_description, public_description_visible, public_display_order
|
|
96
|
+
FROM plan_feature_catalog_overrides
|
|
97
|
+
WHERE tenant_id = ? AND is_custom = 1
|
|
98
|
+
ORDER BY updated_at ASC, created_at ASC, feature_key ASC
|
|
99
|
+
`).all(this.tenantId);
|
|
100
|
+
const features = [];
|
|
101
|
+
for (const row of rows) {
|
|
102
|
+
const featureKey = this.canonicalizePlanFeatureKey(row.feature_key);
|
|
103
|
+
const label = this.normalizeRequiredCatalogLabel(row.label);
|
|
104
|
+
if (!featureKey || !label)
|
|
105
|
+
continue;
|
|
106
|
+
features.push({
|
|
107
|
+
featureKey,
|
|
108
|
+
label,
|
|
109
|
+
description: this.normalizeRequiredCatalogLabel(row.description),
|
|
110
|
+
isCustom: true,
|
|
111
|
+
publicLabel: this.normalizeOptionalCatalogMarketingCopy(row.public_label),
|
|
112
|
+
publicDescription: this.normalizeOptionalCatalogMarketingCopy(row.public_description),
|
|
113
|
+
publicDescriptionVisible: this.normalizeOptionalCatalogDescriptionVisible(row.public_description_visible),
|
|
114
|
+
publicDisplayOrder: this.normalizeOptionalCatalogDisplayOrder(row.public_display_order),
|
|
115
|
+
allowedAccessModes: ['enabled', 'disabled'],
|
|
116
|
+
configTemplate: {}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return features;
|
|
120
|
+
}
|
|
121
|
+
getCustomPlanFeatureCatalogEntry(featureKeyRaw) {
|
|
122
|
+
const featureKey = this.canonicalizePlanFeatureKey(featureKeyRaw);
|
|
123
|
+
if (!featureKey)
|
|
124
|
+
return null;
|
|
125
|
+
const row = this.db.prepare(`
|
|
126
|
+
SELECT feature_key, label, description, public_label, public_description, public_description_visible, public_display_order
|
|
127
|
+
FROM plan_feature_catalog_overrides
|
|
128
|
+
WHERE tenant_id = ? AND feature_key = ? AND is_custom = 1
|
|
129
|
+
LIMIT 1
|
|
130
|
+
`).get(this.tenantId, featureKey);
|
|
131
|
+
if (!row)
|
|
132
|
+
return null;
|
|
133
|
+
const label = this.normalizeRequiredCatalogLabel(row.label);
|
|
134
|
+
if (!label)
|
|
135
|
+
return null;
|
|
136
|
+
return {
|
|
137
|
+
featureKey,
|
|
138
|
+
label,
|
|
139
|
+
description: this.normalizeRequiredCatalogLabel(row.description),
|
|
140
|
+
isCustom: true,
|
|
141
|
+
publicLabel: this.normalizeOptionalCatalogMarketingCopy(row.public_label),
|
|
142
|
+
publicDescription: this.normalizeOptionalCatalogMarketingCopy(row.public_description),
|
|
143
|
+
publicDescriptionVisible: this.normalizeOptionalCatalogDescriptionVisible(row.public_description_visible),
|
|
144
|
+
publicDisplayOrder: this.normalizeOptionalCatalogDisplayOrder(row.public_display_order),
|
|
145
|
+
allowedAccessModes: ['enabled', 'disabled'],
|
|
146
|
+
configTemplate: {}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
52
149
|
listPlanFeatureCatalogOverrideMap() {
|
|
53
150
|
const rows = this.db.prepare(`
|
|
54
|
-
SELECT feature_key, public_label, public_description, public_display_order
|
|
151
|
+
SELECT feature_key, public_label, public_description, public_description_visible, public_display_order, is_custom
|
|
55
152
|
FROM plan_feature_catalog_overrides
|
|
56
153
|
WHERE tenant_id = ?
|
|
57
154
|
`).all(this.tenantId);
|
|
@@ -61,12 +158,15 @@ export class PlanEntitlementService {
|
|
|
61
158
|
const featureKey = this.canonicalizePlanFeatureKey(originalFeatureKey);
|
|
62
159
|
if (!featureKey)
|
|
63
160
|
continue;
|
|
161
|
+
if (Number(row.is_custom || 0) === 1)
|
|
162
|
+
continue;
|
|
64
163
|
if (featureKey === WORKSPACES_FEATURE_KEY && originalFeatureKey === LEGACY_WORKSPACES_FEATURE_KEY && overrides.has(featureKey)) {
|
|
65
164
|
continue;
|
|
66
165
|
}
|
|
67
166
|
overrides.set(featureKey, {
|
|
68
167
|
publicLabel: this.normalizeOptionalCatalogMarketingCopy(row.public_label),
|
|
69
168
|
publicDescription: this.normalizeOptionalCatalogMarketingCopy(row.public_description),
|
|
169
|
+
publicDescriptionVisible: this.normalizeOptionalCatalogDescriptionVisible(row.public_description_visible),
|
|
70
170
|
publicDisplayOrder: this.normalizeOptionalCatalogDisplayOrder(row.public_display_order)
|
|
71
171
|
});
|
|
72
172
|
}
|
|
@@ -689,6 +789,7 @@ export class PlanEntitlementService {
|
|
|
689
789
|
WHERE bpm.tenant_id = ?
|
|
690
790
|
AND pv.plan_id = ?
|
|
691
791
|
AND bpm.active = 1
|
|
792
|
+
AND COALESCE(bpm.pricing_audience, 'public') = 'public'
|
|
692
793
|
`).get(this.tenantId, planId);
|
|
693
794
|
return Number(row?.count || 0) > 0;
|
|
694
795
|
}
|
|
@@ -702,6 +803,7 @@ export class PlanEntitlementService {
|
|
|
702
803
|
WHERE tenant_id = ?
|
|
703
804
|
AND plan_version_id = ?
|
|
704
805
|
AND active = 1
|
|
806
|
+
AND COALESCE(pricing_audience, 'public') = 'public'
|
|
705
807
|
AND pricing_type = 'free'
|
|
706
808
|
`).get(this.tenantId, planVersionId);
|
|
707
809
|
return Number(row?.count || 0) > 0;
|
|
@@ -1054,7 +1156,7 @@ export class PlanEntitlementService {
|
|
|
1054
1156
|
const mappingRow = this.db.prepare(`
|
|
1055
1157
|
SELECT plan_version_id
|
|
1056
1158
|
FROM billing_price_mappings
|
|
1057
|
-
WHERE tenant_id = ? AND stripe_price_id = ? AND active = 1
|
|
1159
|
+
WHERE tenant_id = ? AND stripe_price_id = ? AND active = 1 AND COALESCE(pricing_audience, 'public') = 'public'
|
|
1058
1160
|
ORDER BY updated_at DESC, created_at DESC
|
|
1059
1161
|
LIMIT 1
|
|
1060
1162
|
`).get(this.tenantId, stripePriceId);
|
|
@@ -1490,31 +1592,104 @@ export class PlanEntitlementService {
|
|
|
1490
1592
|
}
|
|
1491
1593
|
listPlanFeatureCatalog() {
|
|
1492
1594
|
const overrides = this.listPlanFeatureCatalogOverrideMap();
|
|
1493
|
-
return
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1595
|
+
return [
|
|
1596
|
+
...DEFAULT_PLAN_FEATURE_CATALOG.map((feature) => ({
|
|
1597
|
+
featureKey: feature.featureKey,
|
|
1598
|
+
label: feature.label,
|
|
1599
|
+
description: feature.description,
|
|
1600
|
+
isCustom: false,
|
|
1601
|
+
publicLabel: overrides.get(feature.featureKey)?.publicLabel ?? feature.publicLabel ?? null,
|
|
1602
|
+
publicDescription: overrides.get(feature.featureKey)?.publicDescription ?? feature.publicDescription ?? null,
|
|
1603
|
+
publicDescriptionVisible: overrides.get(feature.featureKey)?.publicDescriptionVisible ?? feature.publicDescriptionVisible ?? null,
|
|
1604
|
+
publicDisplayOrder: overrides.get(feature.featureKey)?.publicDisplayOrder ?? feature.publicDisplayOrder ?? null,
|
|
1605
|
+
allowedAccessModes: [...feature.allowedAccessModes],
|
|
1606
|
+
configTemplate: { ...feature.configTemplate },
|
|
1607
|
+
...(feature.dependsOn?.length ? { dependsOn: [...feature.dependsOn] } : {})
|
|
1608
|
+
})),
|
|
1609
|
+
...this.listCustomPlanFeatureCatalogEntries()
|
|
1610
|
+
];
|
|
1611
|
+
}
|
|
1612
|
+
createCustomPlanFeatureCatalogEntry(input) {
|
|
1613
|
+
const label = this.normalizeRequiredCatalogLabel(input.label);
|
|
1614
|
+
const description = this.normalizeRequiredCatalogLabel(input.description);
|
|
1615
|
+
if (!label)
|
|
1616
|
+
throw new TaskforceRuleError('Feature label is required', 400, 'PLAN_FEATURE_LABEL_REQUIRED');
|
|
1617
|
+
const featureKey = this.generateUniqueCustomPlanFeatureKey(label);
|
|
1618
|
+
const publicLabel = this.normalizeOptionalCatalogMarketingCopy(input.publicLabel);
|
|
1619
|
+
const publicDescription = this.normalizeOptionalCatalogMarketingCopy(input.publicDescription);
|
|
1620
|
+
const publicDescriptionVisible = this.normalizeOptionalCatalogDescriptionVisible(input.publicDescriptionVisible);
|
|
1621
|
+
const publicDisplayOrder = this.normalizeOptionalCatalogDisplayOrder(input.publicDisplayOrder);
|
|
1622
|
+
const now = new Date().toISOString();
|
|
1623
|
+
this.db.prepare(`
|
|
1624
|
+
INSERT INTO plan_feature_catalog_overrides (
|
|
1625
|
+
tenant_id,
|
|
1626
|
+
feature_key,
|
|
1627
|
+
label,
|
|
1628
|
+
description,
|
|
1629
|
+
is_custom,
|
|
1630
|
+
public_label,
|
|
1631
|
+
public_description,
|
|
1632
|
+
public_description_visible,
|
|
1633
|
+
public_display_order,
|
|
1634
|
+
created_at,
|
|
1635
|
+
updated_at
|
|
1636
|
+
)
|
|
1637
|
+
VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?)
|
|
1638
|
+
`).run(this.tenantId, featureKey, label, description, publicLabel, publicDescription, publicDescriptionVisible === null ? null : (publicDescriptionVisible ? 1 : 0), publicDisplayOrder, now, now);
|
|
1639
|
+
return this.getCustomPlanFeatureCatalogEntry(featureKey) || {
|
|
1640
|
+
featureKey,
|
|
1641
|
+
label,
|
|
1642
|
+
description,
|
|
1643
|
+
isCustom: true,
|
|
1644
|
+
publicLabel,
|
|
1645
|
+
publicDescription,
|
|
1646
|
+
publicDescriptionVisible,
|
|
1647
|
+
publicDisplayOrder,
|
|
1648
|
+
allowedAccessModes: ['enabled', 'disabled'],
|
|
1649
|
+
configTemplate: {}
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
deleteCustomPlanFeatureCatalogEntry(featureKeyRaw) {
|
|
1653
|
+
const featureKey = this.canonicalizePlanFeatureKey(featureKeyRaw);
|
|
1654
|
+
if (!featureKey)
|
|
1655
|
+
throw new TaskforceRuleError('featureKey is required', 400, 'FEATURE_KEY_REQUIRED');
|
|
1656
|
+
if (this.isBuiltInPlanFeatureKey(featureKey)) {
|
|
1657
|
+
throw new TaskforceRuleError('Built-in plan features cannot be deleted', 400, 'PLAN_FEATURE_DELETE_BUILTIN_FORBIDDEN');
|
|
1658
|
+
}
|
|
1659
|
+
const existing = this.getCustomPlanFeatureCatalogEntry(featureKey);
|
|
1660
|
+
if (!existing)
|
|
1661
|
+
throw new TaskforceRuleError('Unknown custom plan feature key', 404, 'PLAN_FEATURE_NOT_FOUND');
|
|
1662
|
+
const tx = this.db.transaction(() => {
|
|
1663
|
+
this.db.prepare(`
|
|
1664
|
+
DELETE FROM plan_feature_catalog_overrides
|
|
1665
|
+
WHERE tenant_id = ? AND feature_key = ? AND is_custom = 1
|
|
1666
|
+
`).run(this.tenantId, featureKey);
|
|
1667
|
+
this.db.prepare(`
|
|
1668
|
+
DELETE FROM plan_feature_matrix
|
|
1669
|
+
WHERE tenant_id = ? AND feature_key = ?
|
|
1670
|
+
`).run(this.tenantId, featureKey);
|
|
1671
|
+
});
|
|
1672
|
+
tx();
|
|
1504
1673
|
}
|
|
1505
1674
|
updatePlanFeatureCatalogMarketingCopy(input) {
|
|
1506
1675
|
const featureKey = this.canonicalizePlanFeatureKey(input.featureKey);
|
|
1507
1676
|
if (!featureKey)
|
|
1508
1677
|
throw new TaskforceRuleError('featureKey is required', 400, 'FEATURE_KEY_REQUIRED');
|
|
1509
1678
|
const catalogFeature = DEFAULT_PLAN_FEATURE_CATALOG.find((feature) => feature.featureKey === featureKey);
|
|
1510
|
-
|
|
1679
|
+
const customFeature = this.getCustomPlanFeatureCatalogEntry(featureKey);
|
|
1680
|
+
if (!catalogFeature && !customFeature)
|
|
1511
1681
|
throw new TaskforceRuleError('Unknown plan feature key', 404, 'PLAN_FEATURE_NOT_FOUND');
|
|
1682
|
+
const label = this.normalizeRequiredCatalogLabel(input.label ?? customFeature?.label ?? '');
|
|
1683
|
+
const description = this.normalizeRequiredCatalogLabel(input.description ?? customFeature?.description ?? '');
|
|
1684
|
+
if (customFeature && !label)
|
|
1685
|
+
throw new TaskforceRuleError('Feature label is required', 400, 'PLAN_FEATURE_LABEL_REQUIRED');
|
|
1512
1686
|
const publicLabel = this.normalizeOptionalCatalogMarketingCopy(input.publicLabel);
|
|
1513
1687
|
const publicDescription = this.normalizeOptionalCatalogMarketingCopy(input.publicDescription);
|
|
1688
|
+
const publicDescriptionVisible = this.normalizeOptionalCatalogDescriptionVisible(input.publicDescriptionVisible);
|
|
1514
1689
|
const publicDisplayOrder = this.normalizeOptionalCatalogDisplayOrder(input.publicDisplayOrder);
|
|
1515
1690
|
const now = new Date().toISOString();
|
|
1516
1691
|
const tx = this.db.transaction(() => {
|
|
1517
|
-
if (!publicLabel && !publicDescription && publicDisplayOrder === null) {
|
|
1692
|
+
if (!customFeature && !publicLabel && !publicDescription && publicDescriptionVisible === null && publicDisplayOrder === null) {
|
|
1518
1693
|
this.db.prepare(`
|
|
1519
1694
|
DELETE FROM plan_feature_catalog_overrides
|
|
1520
1695
|
WHERE tenant_id = ? AND feature_key = ?
|
|
@@ -1525,32 +1700,53 @@ export class PlanEntitlementService {
|
|
|
1525
1700
|
INSERT INTO plan_feature_catalog_overrides (
|
|
1526
1701
|
tenant_id,
|
|
1527
1702
|
feature_key,
|
|
1703
|
+
label,
|
|
1704
|
+
description,
|
|
1705
|
+
is_custom,
|
|
1528
1706
|
public_label,
|
|
1529
1707
|
public_description,
|
|
1708
|
+
public_description_visible,
|
|
1530
1709
|
public_display_order,
|
|
1531
1710
|
created_at,
|
|
1532
1711
|
updated_at
|
|
1533
1712
|
)
|
|
1534
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1713
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1535
1714
|
ON CONFLICT (tenant_id, feature_key) DO UPDATE SET
|
|
1715
|
+
label = excluded.label,
|
|
1716
|
+
description = excluded.description,
|
|
1717
|
+
is_custom = excluded.is_custom,
|
|
1536
1718
|
public_label = excluded.public_label,
|
|
1537
1719
|
public_description = excluded.public_description,
|
|
1720
|
+
public_description_visible = excluded.public_description_visible,
|
|
1538
1721
|
public_display_order = excluded.public_display_order,
|
|
1539
1722
|
updated_at = excluded.updated_at
|
|
1540
|
-
`).run(this.tenantId, featureKey, publicLabel, publicDescription, publicDisplayOrder, now, now);
|
|
1723
|
+
`).run(this.tenantId, featureKey, customFeature ? label : null, customFeature ? description : null, customFeature ? 1 : 0, publicLabel, publicDescription, publicDescriptionVisible === null ? null : (publicDescriptionVisible ? 1 : 0), publicDisplayOrder, now, now);
|
|
1541
1724
|
});
|
|
1542
1725
|
tx();
|
|
1543
|
-
return this.listPlanFeatureCatalog().find((feature) => feature.featureKey === featureKey) || {
|
|
1726
|
+
return this.listPlanFeatureCatalog().find((feature) => feature.featureKey === featureKey) || (customFeature ? {
|
|
1727
|
+
featureKey,
|
|
1728
|
+
label,
|
|
1729
|
+
description,
|
|
1730
|
+
isCustom: true,
|
|
1731
|
+
publicLabel,
|
|
1732
|
+
publicDescription,
|
|
1733
|
+
publicDescriptionVisible,
|
|
1734
|
+
publicDisplayOrder,
|
|
1735
|
+
allowedAccessModes: ['enabled', 'disabled'],
|
|
1736
|
+
configTemplate: {}
|
|
1737
|
+
} : {
|
|
1544
1738
|
featureKey: catalogFeature.featureKey,
|
|
1545
1739
|
label: catalogFeature.label,
|
|
1546
1740
|
description: catalogFeature.description,
|
|
1741
|
+
isCustom: false,
|
|
1547
1742
|
publicLabel,
|
|
1548
1743
|
publicDescription,
|
|
1744
|
+
publicDescriptionVisible,
|
|
1549
1745
|
publicDisplayOrder,
|
|
1550
1746
|
allowedAccessModes: [...catalogFeature.allowedAccessModes],
|
|
1551
1747
|
configTemplate: { ...catalogFeature.configTemplate },
|
|
1552
1748
|
...(catalogFeature.dependsOn?.length ? { dependsOn: [...catalogFeature.dependsOn] } : {})
|
|
1553
|
-
};
|
|
1749
|
+
});
|
|
1554
1750
|
}
|
|
1555
1751
|
listPlanVersions(planId) {
|
|
1556
1752
|
const normalizedPlanId = String(planId || '').trim().toLowerCase();
|
|
@@ -17,33 +17,40 @@ describe('TaskforceCore plan feature catalog marketing copy', () => {
|
|
|
17
17
|
expect(initialFeature?.label).toBe('AI Profiles');
|
|
18
18
|
expect(initialFeature?.publicLabel ?? null).toBeNull();
|
|
19
19
|
expect(initialFeature?.publicDescription ?? null).toBeNull();
|
|
20
|
+
expect(initialFeature?.publicDescriptionVisible ?? null).toBeNull();
|
|
20
21
|
expect(initialFeature?.publicDisplayOrder).toBe(20);
|
|
21
22
|
const updated = core.updatePlanFeatureCatalogMarketingCopy({
|
|
22
23
|
featureKey: 'collaboration.ai_profiles',
|
|
23
24
|
publicLabel: 'AI teammates',
|
|
24
25
|
publicDescription: 'Add reusable AI teammates to your workspace.',
|
|
26
|
+
publicDescriptionVisible: false,
|
|
25
27
|
publicDisplayOrder: 24
|
|
26
28
|
});
|
|
27
29
|
expect(updated.publicLabel).toBe('AI teammates');
|
|
28
30
|
expect(updated.publicDescription).toBe('Add reusable AI teammates to your workspace.');
|
|
31
|
+
expect(updated.publicDescriptionVisible).toBe(false);
|
|
29
32
|
expect(updated.publicDisplayOrder).toBe(24);
|
|
30
33
|
const persisted = core.listPlanFeatureCatalog().find((feature) => feature.featureKey === 'collaboration.ai_profiles');
|
|
31
34
|
expect(persisted?.publicLabel).toBe('AI teammates');
|
|
32
35
|
expect(persisted?.publicDescription).toBe('Add reusable AI teammates to your workspace.');
|
|
36
|
+
expect(persisted?.publicDescriptionVisible).toBe(false);
|
|
33
37
|
expect(persisted?.publicDisplayOrder).toBe(24);
|
|
34
38
|
const cleared = core.updatePlanFeatureCatalogMarketingCopy({
|
|
35
39
|
featureKey: 'collaboration.ai_profiles',
|
|
36
40
|
publicLabel: ' ',
|
|
37
41
|
publicDescription: '',
|
|
42
|
+
publicDescriptionVisible: null,
|
|
38
43
|
publicDisplayOrder: null
|
|
39
44
|
});
|
|
40
45
|
expect(cleared.publicLabel ?? null).toBeNull();
|
|
41
46
|
expect(cleared.publicDescription ?? null).toBeNull();
|
|
47
|
+
expect(cleared.publicDescriptionVisible ?? null).toBeNull();
|
|
42
48
|
expect(cleared.publicDisplayOrder).toBe(20);
|
|
43
49
|
const fallback = core.listPlanFeatureCatalog().find((feature) => feature.featureKey === 'collaboration.ai_profiles');
|
|
44
50
|
expect(fallback?.label).toBe('AI Profiles');
|
|
45
51
|
expect(fallback?.publicLabel ?? null).toBeNull();
|
|
46
52
|
expect(fallback?.publicDescription ?? null).toBeNull();
|
|
53
|
+
expect(fallback?.publicDescriptionVisible ?? null).toBeNull();
|
|
47
54
|
expect(fallback?.publicDisplayOrder).toBe(20);
|
|
48
55
|
core.close();
|
|
49
56
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
@@ -81,4 +88,52 @@ describe('TaskforceCore plan feature catalog marketing copy', () => {
|
|
|
81
88
|
core.close();
|
|
82
89
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
83
90
|
});
|
|
91
|
+
it('creates, updates, and deletes custom feature catalog entries', () => {
|
|
92
|
+
const projectRoot = makeProjectRoot();
|
|
93
|
+
const core = new TaskforceCore({
|
|
94
|
+
projectRoot,
|
|
95
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
96
|
+
});
|
|
97
|
+
const created = core.createCustomPlanFeatureCatalogEntry({
|
|
98
|
+
label: 'Unlimited Local Workspaces',
|
|
99
|
+
description: 'Run unlimited local workspaces on your device.',
|
|
100
|
+
publicDescriptionVisible: false,
|
|
101
|
+
publicDisplayOrder: 12
|
|
102
|
+
});
|
|
103
|
+
expect(created.featureKey).toMatch(/^marketing\./);
|
|
104
|
+
expect(created.isCustom).toBe(true);
|
|
105
|
+
expect(created.label).toBe('Unlimited Local Workspaces');
|
|
106
|
+
expect(created.description).toBe('Run unlimited local workspaces on your device.');
|
|
107
|
+
expect(created.publicDescriptionVisible).toBe(false);
|
|
108
|
+
expect(created.publicDisplayOrder).toBe(12);
|
|
109
|
+
const updated = core.updatePlanFeatureCatalogMarketingCopy({
|
|
110
|
+
featureKey: created.featureKey,
|
|
111
|
+
label: 'Unlimited Local Workspaces',
|
|
112
|
+
description: 'Run unlimited local workspaces offline on your device.',
|
|
113
|
+
publicLabel: 'Unlimited Local Workspaces',
|
|
114
|
+
publicDescription: 'Create as many fully local workspaces as you need.',
|
|
115
|
+
publicDescriptionVisible: true,
|
|
116
|
+
publicDisplayOrder: 14
|
|
117
|
+
});
|
|
118
|
+
expect(updated.isCustom).toBe(true);
|
|
119
|
+
expect(updated.description).toBe('Run unlimited local workspaces offline on your device.');
|
|
120
|
+
expect(updated.publicLabel).toBe('Unlimited Local Workspaces');
|
|
121
|
+
expect(updated.publicDescription).toBe('Create as many fully local workspaces as you need.');
|
|
122
|
+
expect(updated.publicDescriptionVisible).toBe(true);
|
|
123
|
+
expect(updated.publicDisplayOrder).toBe(14);
|
|
124
|
+
const plan = core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
|
|
125
|
+
expect(plan.planId).toBe('free');
|
|
126
|
+
core.updatePlanVersionFeatures({
|
|
127
|
+
planVersionId: 'free-v1',
|
|
128
|
+
features: [
|
|
129
|
+
{ featureKey: created.featureKey, access: 'enabled', config: {} }
|
|
130
|
+
]
|
|
131
|
+
});
|
|
132
|
+
expect(core.getPlanVersionFeatures('free', 'free-v1').features[created.featureKey]?.access).toBe('enabled');
|
|
133
|
+
core.deleteCustomPlanFeatureCatalogEntry(created.featureKey);
|
|
134
|
+
expect(core.listPlanFeatureCatalog().some((feature) => feature.featureKey === created.featureKey)).toBe(false);
|
|
135
|
+
expect(core.getPlanVersionFeatures('free', 'free-v1').features[created.featureKey]).toBeUndefined();
|
|
136
|
+
core.close();
|
|
137
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
138
|
+
});
|
|
84
139
|
});
|
package/dist/core/Taskforce.d.ts
CHANGED
|
@@ -617,6 +617,8 @@ export interface PlanFeatureCatalogItem {
|
|
|
617
617
|
featureKey: string;
|
|
618
618
|
label: string;
|
|
619
619
|
description: string;
|
|
620
|
+
isCustom?: boolean;
|
|
621
|
+
publicDescriptionVisible?: boolean | null;
|
|
620
622
|
allowedAccessModes: PlanFeatureAccess[];
|
|
621
623
|
configTemplate: Record<string, unknown>;
|
|
622
624
|
dependsOn?: string[];
|
|
@@ -1618,10 +1620,22 @@ export declare class TaskforceCore implements TaskforceSyncCapable {
|
|
|
1618
1620
|
};
|
|
1619
1621
|
listPlanCatalog(): import("./types.js").PlanCatalogSnapshot[];
|
|
1620
1622
|
listPlanFeatureCatalog(): import("./types.js").PlanFeatureCatalogItem[];
|
|
1623
|
+
createCustomPlanFeatureCatalogEntry(input: {
|
|
1624
|
+
label: string;
|
|
1625
|
+
description?: string | null;
|
|
1626
|
+
publicLabel?: string | null;
|
|
1627
|
+
publicDescription?: string | null;
|
|
1628
|
+
publicDescriptionVisible?: boolean | null;
|
|
1629
|
+
publicDisplayOrder?: number | null;
|
|
1630
|
+
}): import("./types.js").PlanFeatureCatalogItem;
|
|
1631
|
+
deleteCustomPlanFeatureCatalogEntry(featureKey: string): void;
|
|
1621
1632
|
updatePlanFeatureCatalogMarketingCopy(input: {
|
|
1622
1633
|
featureKey: string;
|
|
1634
|
+
label?: string | null;
|
|
1635
|
+
description?: string | null;
|
|
1623
1636
|
publicLabel?: string | null;
|
|
1624
1637
|
publicDescription?: string | null;
|
|
1638
|
+
publicDescriptionVisible?: boolean | null;
|
|
1625
1639
|
publicDisplayOrder?: number | null;
|
|
1626
1640
|
}): import("./types.js").PlanFeatureCatalogItem;
|
|
1627
1641
|
listPlanVersions(planId: string): import("./types.js").PlanVersionSnapshot[];
|
package/dist/core/Taskforce.js
CHANGED
|
@@ -6219,6 +6219,12 @@ export class TaskforceCore {
|
|
|
6219
6219
|
listPlanFeatureCatalog() {
|
|
6220
6220
|
return this.planEntitlementService.listPlanFeatureCatalog();
|
|
6221
6221
|
}
|
|
6222
|
+
createCustomPlanFeatureCatalogEntry(input) {
|
|
6223
|
+
return this.planEntitlementService.createCustomPlanFeatureCatalogEntry(input);
|
|
6224
|
+
}
|
|
6225
|
+
deleteCustomPlanFeatureCatalogEntry(featureKey) {
|
|
6226
|
+
return this.planEntitlementService.deleteCustomPlanFeatureCatalogEntry(featureKey);
|
|
6227
|
+
}
|
|
6222
6228
|
updatePlanFeatureCatalogMarketingCopy(input) {
|
|
6223
6229
|
return this.planEntitlementService.updatePlanFeatureCatalogMarketingCopy(input);
|
|
6224
6230
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -279,6 +279,7 @@ export interface GlobalTaskforceSettings {
|
|
|
279
279
|
siteTheme?: 'dark' | 'light';
|
|
280
280
|
pricingPageTitle?: string;
|
|
281
281
|
pricingPageDescription?: string;
|
|
282
|
+
pricingPageShowPublicDescriptions?: boolean;
|
|
282
283
|
};
|
|
283
284
|
setup?: {
|
|
284
285
|
mode?: 'core' | 'operations';
|
|
@@ -509,8 +510,10 @@ export interface PlanFeatureCatalogItem {
|
|
|
509
510
|
featureKey: string;
|
|
510
511
|
label: string;
|
|
511
512
|
description: string;
|
|
513
|
+
isCustom?: boolean;
|
|
512
514
|
publicLabel?: string | null;
|
|
513
515
|
publicDescription?: string | null;
|
|
516
|
+
publicDescriptionVisible?: boolean | null;
|
|
514
517
|
publicDisplayOrder?: number | null;
|
|
515
518
|
allowedAccessModes: PlanFeatureAccess[];
|
|
516
519
|
configTemplate: Record<string, unknown>;
|
|
@@ -476,6 +476,7 @@ export function ensureBillingSchema(db) {
|
|
|
476
476
|
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
477
477
|
plan_version_id TEXT NOT NULL,
|
|
478
478
|
billing_interval TEXT NOT NULL,
|
|
479
|
+
pricing_audience TEXT NOT NULL DEFAULT 'public',
|
|
479
480
|
stripe_price_id TEXT NOT NULL,
|
|
480
481
|
pricing_type TEXT NOT NULL DEFAULT 'stripe',
|
|
481
482
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
@@ -483,7 +484,7 @@ export function ensureBillingSchema(db) {
|
|
|
483
484
|
currency TEXT,
|
|
484
485
|
created_at TEXT NOT NULL,
|
|
485
486
|
updated_at TEXT NOT NULL,
|
|
486
|
-
PRIMARY KEY (tenant_id, plan_version_id, billing_interval, stripe_price_id)
|
|
487
|
+
PRIMARY KEY (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id)
|
|
487
488
|
);
|
|
488
489
|
|
|
489
490
|
CREATE INDEX IF NOT EXISTS idx_user_billing_tenant ON user_billing(tenant_id);
|
|
@@ -501,6 +502,7 @@ export function ensureBillingSchema(db) {
|
|
|
501
502
|
const hasUnitAmountColumn = columns.some((c) => c.name === 'unit_amount');
|
|
502
503
|
const hasCurrencyColumn = columns.some((c) => c.name === 'currency');
|
|
503
504
|
const hasPricingTypeColumn = columns.some((c) => c.name === 'pricing_type');
|
|
505
|
+
const hasPricingAudienceColumn = columns.some((c) => c.name === 'pricing_audience');
|
|
504
506
|
if (!hasUnitAmountColumn) {
|
|
505
507
|
db.exec(`ALTER TABLE billing_price_mappings ADD COLUMN unit_amount INTEGER`);
|
|
506
508
|
}
|
|
@@ -510,30 +512,34 @@ export function ensureBillingSchema(db) {
|
|
|
510
512
|
if (!hasPricingTypeColumn) {
|
|
511
513
|
db.exec(`ALTER TABLE billing_price_mappings ADD COLUMN pricing_type TEXT NOT NULL DEFAULT 'stripe'`);
|
|
512
514
|
}
|
|
515
|
+
if (!hasPricingAudienceColumn) {
|
|
516
|
+
db.exec(`ALTER TABLE billing_price_mappings ADD COLUMN pricing_audience TEXT NOT NULL DEFAULT 'public'`);
|
|
517
|
+
}
|
|
513
518
|
ensureBillingPriceMappingsConstraints(db);
|
|
514
519
|
normalizeUserBillingDefaults(db);
|
|
515
520
|
}
|
|
516
521
|
function ensureBillingPriceMappingsConstraints(db) {
|
|
517
522
|
if (db.provider === 'postgres') {
|
|
518
523
|
db.exec(`
|
|
524
|
+
ALTER TABLE billing_price_mappings ADD COLUMN IF NOT EXISTS pricing_audience TEXT NOT NULL DEFAULT 'public';
|
|
525
|
+
DROP INDEX IF EXISTS idx_billing_price_mappings_active_interval;
|
|
519
526
|
ALTER TABLE billing_price_mappings DROP CONSTRAINT IF EXISTS billing_price_mappings_pkey;
|
|
520
527
|
ALTER TABLE billing_price_mappings
|
|
521
528
|
ADD CONSTRAINT billing_price_mappings_pkey
|
|
522
|
-
PRIMARY KEY (tenant_id, plan_version_id, billing_interval, stripe_price_id);
|
|
529
|
+
PRIMARY KEY (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id);
|
|
523
530
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_billing_price_mappings_active_interval
|
|
524
|
-
ON billing_price_mappings(tenant_id, plan_version_id, billing_interval)
|
|
531
|
+
ON billing_price_mappings(tenant_id, plan_version_id, billing_interval, pricing_audience)
|
|
525
532
|
WHERE active = TRUE;
|
|
526
533
|
`);
|
|
527
534
|
return;
|
|
528
535
|
}
|
|
529
536
|
const indexes = db.prepare(`PRAGMA index_list(billing_price_mappings)`).all();
|
|
530
537
|
const tableInfo = db.prepare(`PRAGMA table_info(billing_price_mappings)`).all();
|
|
531
|
-
const hasActiveIntervalIndex = indexes.some((index) => index.name === 'idx_billing_price_mappings_active_interval');
|
|
532
538
|
const primaryKeyColumns = tableInfo
|
|
533
539
|
.filter((column) => Number(column.pk || 0) > 0)
|
|
534
540
|
.sort((a, b) => Number(a.pk || 0) - Number(b.pk || 0))
|
|
535
541
|
.map((column) => String(column.name || ''));
|
|
536
|
-
const needsPrimaryKeyRepair = primaryKeyColumns.join('|') !== 'tenant_id|plan_version_id|billing_interval|stripe_price_id';
|
|
542
|
+
const needsPrimaryKeyRepair = primaryKeyColumns.join('|') !== 'tenant_id|plan_version_id|billing_interval|pricing_audience|stripe_price_id';
|
|
537
543
|
if (needsPrimaryKeyRepair) {
|
|
538
544
|
db.exec(`
|
|
539
545
|
ALTER TABLE billing_price_mappings RENAME TO billing_price_mappings_legacy;
|
|
@@ -542,6 +548,7 @@ function ensureBillingPriceMappingsConstraints(db) {
|
|
|
542
548
|
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
543
549
|
plan_version_id TEXT NOT NULL,
|
|
544
550
|
billing_interval TEXT NOT NULL,
|
|
551
|
+
pricing_audience TEXT NOT NULL DEFAULT 'public',
|
|
545
552
|
stripe_price_id TEXT NOT NULL,
|
|
546
553
|
pricing_type TEXT NOT NULL DEFAULT 'stripe',
|
|
547
554
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
@@ -549,26 +556,25 @@ function ensureBillingPriceMappingsConstraints(db) {
|
|
|
549
556
|
currency TEXT,
|
|
550
557
|
created_at TEXT NOT NULL,
|
|
551
558
|
updated_at TEXT NOT NULL,
|
|
552
|
-
PRIMARY KEY (tenant_id, plan_version_id, billing_interval, stripe_price_id)
|
|
559
|
+
PRIMARY KEY (tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id)
|
|
553
560
|
);
|
|
554
561
|
|
|
555
562
|
INSERT INTO billing_price_mappings (
|
|
556
|
-
tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
563
|
+
tenant_id, plan_version_id, billing_interval, pricing_audience, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
|
|
557
564
|
)
|
|
558
565
|
SELECT
|
|
559
|
-
tenant_id, plan_version_id, billing_interval, stripe_price_id, COALESCE(pricing_type, 'stripe'), active, unit_amount, currency, created_at, updated_at
|
|
566
|
+
tenant_id, plan_version_id, billing_interval, COALESCE(pricing_audience, 'public'), stripe_price_id, COALESCE(pricing_type, 'stripe'), active, unit_amount, currency, created_at, updated_at
|
|
560
567
|
FROM billing_price_mappings_legacy;
|
|
561
568
|
|
|
562
569
|
DROP TABLE billing_price_mappings_legacy;
|
|
563
570
|
`);
|
|
564
571
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
+
db.exec(`
|
|
573
|
+
DROP INDEX IF EXISTS idx_billing_price_mappings_active_interval;
|
|
574
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_billing_price_mappings_active_interval
|
|
575
|
+
ON billing_price_mappings(tenant_id, plan_version_id, billing_interval, pricing_audience)
|
|
576
|
+
WHERE active = 1;
|
|
577
|
+
`);
|
|
572
578
|
}
|
|
573
579
|
function normalizeUserBillingDefaults(db) {
|
|
574
580
|
if (db.provider === 'postgres') {
|
|
@@ -691,8 +697,12 @@ export function ensureEntitlementsSchema(db) {
|
|
|
691
697
|
CREATE TABLE IF NOT EXISTS plan_feature_catalog_overrides (
|
|
692
698
|
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
693
699
|
feature_key TEXT NOT NULL,
|
|
700
|
+
label TEXT,
|
|
701
|
+
description TEXT,
|
|
702
|
+
is_custom INTEGER NOT NULL DEFAULT 0,
|
|
694
703
|
public_label TEXT,
|
|
695
704
|
public_description TEXT,
|
|
705
|
+
public_description_visible INTEGER,
|
|
696
706
|
public_display_order INTEGER,
|
|
697
707
|
created_at TEXT NOT NULL,
|
|
698
708
|
updated_at TEXT NOT NULL,
|
|
@@ -754,6 +764,18 @@ export function ensureEntitlementsSchema(db) {
|
|
|
754
764
|
db.exec(`ALTER TABLE plan_catalog ADD COLUMN description TEXT`);
|
|
755
765
|
}
|
|
756
766
|
const planFeatureCatalogOverrideColumns = db.prepare(`PRAGMA table_info(plan_feature_catalog_overrides)`).all();
|
|
767
|
+
if (!planFeatureCatalogOverrideColumns.some((column) => column.name === 'label')) {
|
|
768
|
+
db.exec(`ALTER TABLE plan_feature_catalog_overrides ADD COLUMN label TEXT`);
|
|
769
|
+
}
|
|
770
|
+
if (!planFeatureCatalogOverrideColumns.some((column) => column.name === 'description')) {
|
|
771
|
+
db.exec(`ALTER TABLE plan_feature_catalog_overrides ADD COLUMN description TEXT`);
|
|
772
|
+
}
|
|
773
|
+
if (!planFeatureCatalogOverrideColumns.some((column) => column.name === 'is_custom')) {
|
|
774
|
+
db.exec(`ALTER TABLE plan_feature_catalog_overrides ADD COLUMN is_custom INTEGER NOT NULL DEFAULT 0`);
|
|
775
|
+
}
|
|
776
|
+
if (!planFeatureCatalogOverrideColumns.some((column) => column.name === 'public_description_visible')) {
|
|
777
|
+
db.exec(`ALTER TABLE plan_feature_catalog_overrides ADD COLUMN public_description_visible INTEGER`);
|
|
778
|
+
}
|
|
757
779
|
if (!planFeatureCatalogOverrideColumns.some((column) => column.name === 'public_display_order')) {
|
|
758
780
|
db.exec(`ALTER TABLE plan_feature_catalog_overrides ADD COLUMN public_display_order INTEGER`);
|
|
759
781
|
}
|