@taskforcehq/taskforce 0.3.302 → 0.3.304
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/Taskforce.module.css +113 -5
- package/dist/TaskforceCore.js +560 -208
- package/dist/TaskforceCore.test.js +1833 -91
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +7 -1
- package/dist/components/views/PlansPage.js +139 -53
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +157 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +216 -186
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
- package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +31 -5
- package/dist/core/PlanEntitlementService.js +350 -222
- package/dist/core/SignupMonetizationSettings.test.js +208 -49
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/Taskforce.d.ts +65 -20
- package/dist/core/Taskforce.js +261 -51
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/core/types.d.ts +11 -12
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +61 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +122 -0
- package/dist/hooks/sync/transfers.js +431 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
- package/dist/hooks/useSyncOrchestrator.js +315 -860
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +11 -2
- package/dist/hooks/useTaskforce.js +290 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +264 -505
- package/dist/mcp/runtime.test.js +353 -89
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +15 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +48 -3
- package/dist/server/index.test.js +14 -2
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +91 -40
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +231 -55
- package/dist/server/routes/billing.js +473 -39
- package/dist/server/routes/billing.test.js +972 -37
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +31 -3
- package/dist/server/routes/sync.integration.test.js +180 -0
- package/dist/server/routes/sync.js +125 -242
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +11 -22
- package/dist/server/routes.test.js +530 -81
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.d.ts +10 -0
- package/dist/sync/contentSyncState.js +277 -14
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +377 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +57 -10
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +11 -0
- package/dist/sync/workspacePullFeed.js +123 -68
- package/dist/sync/workspacePullFeed.test.js +180 -0
- package/dist/sync/workspaceSyncModel.d.ts +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +5 -3
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
- package/dist/ui/assets/index-MXeWoebC.css +0 -1
- package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
- package/dist/utils/billingStatusCache.d.ts +0 -21
- package/dist/utils/billingStatusCache.js +0 -70
|
@@ -35,7 +35,13 @@ export class PlanEntitlementService {
|
|
|
35
35
|
// ─── Normalizers ─────────────────────────────────────────────────────────
|
|
36
36
|
normalizeEntitlementState(value) {
|
|
37
37
|
const normalized = String(value || '').trim().toLowerCase();
|
|
38
|
-
if (normalized === '
|
|
38
|
+
if (normalized === 'pending_plan_selection'
|
|
39
|
+
|| normalized === 'checkout_pending'
|
|
40
|
+
|| normalized === 'active'
|
|
41
|
+
|| normalized === 'trialing'
|
|
42
|
+
|| normalized === 'grace'
|
|
43
|
+
|| normalized === 'suspended'
|
|
44
|
+
|| normalized === 'canceled') {
|
|
39
45
|
return normalized;
|
|
40
46
|
}
|
|
41
47
|
return 'canceled';
|
|
@@ -112,6 +118,77 @@ export class PlanEntitlementService {
|
|
|
112
118
|
isEntitlementStateAllowed(state) {
|
|
113
119
|
return state === 'active' || state === 'trialing' || state === 'grace';
|
|
114
120
|
}
|
|
121
|
+
isIsoTimestampExpired(value) {
|
|
122
|
+
const timestamp = String(value || '').trim();
|
|
123
|
+
if (!timestamp)
|
|
124
|
+
return false;
|
|
125
|
+
const expiresAt = Date.parse(timestamp);
|
|
126
|
+
if (!Number.isFinite(expiresAt))
|
|
127
|
+
return false;
|
|
128
|
+
return expiresAt <= Date.now();
|
|
129
|
+
}
|
|
130
|
+
readAccountEntitlementRow(accountId) {
|
|
131
|
+
return this.db.prepare(`
|
|
132
|
+
SELECT account_id, plan_version_id, entitlement_state, effective_at, effective_until
|
|
133
|
+
FROM account_entitlements
|
|
134
|
+
WHERE tenant_id = ? AND account_id = ?
|
|
135
|
+
LIMIT 1
|
|
136
|
+
`).get(this.tenantId, accountId);
|
|
137
|
+
}
|
|
138
|
+
normalizeExpiredCheckoutPendingRow(accountId) {
|
|
139
|
+
const row = this.readAccountEntitlementRow(accountId);
|
|
140
|
+
if (!row?.account_id || !row.plan_version_id) {
|
|
141
|
+
return row;
|
|
142
|
+
}
|
|
143
|
+
const currentState = this.normalizeEntitlementState(row.entitlement_state);
|
|
144
|
+
if (currentState !== 'checkout_pending' || !this.isIsoTimestampExpired(row.effective_until || null)) {
|
|
145
|
+
return row;
|
|
146
|
+
}
|
|
147
|
+
const now = new Date().toISOString();
|
|
148
|
+
this.db.prepare(`
|
|
149
|
+
UPDATE account_entitlements
|
|
150
|
+
SET entitlement_state = ?, effective_until = NULL, updated_by = ?, updated_at = ?
|
|
151
|
+
WHERE tenant_id = ? AND account_id = ?
|
|
152
|
+
`).run('pending_plan_selection', 'checkout-expired', now, this.tenantId, accountId);
|
|
153
|
+
this.createUserBillingRecord(accountId, {
|
|
154
|
+
entitlementState: 'pending_plan_selection',
|
|
155
|
+
planId: null,
|
|
156
|
+
effectiveUntil: null
|
|
157
|
+
});
|
|
158
|
+
return this.readAccountEntitlementRow(accountId);
|
|
159
|
+
}
|
|
160
|
+
isBillingConfirmedTransitionSource(source) {
|
|
161
|
+
return source === 'checkout-confirm' || source === 'stripe-webhook';
|
|
162
|
+
}
|
|
163
|
+
resolveCanonicalTransitionSource(updatedBy) {
|
|
164
|
+
if (updatedBy === 'registration'
|
|
165
|
+
|| updatedBy === 'checkout-start'
|
|
166
|
+
|| updatedBy === 'checkout-confirm'
|
|
167
|
+
|| updatedBy === 'stripe-webhook'
|
|
168
|
+
|| updatedBy === 'backfill'
|
|
169
|
+
|| updatedBy === 'legacy-plan-mode') {
|
|
170
|
+
return updatedBy;
|
|
171
|
+
}
|
|
172
|
+
return 'system';
|
|
173
|
+
}
|
|
174
|
+
resolvePendingPlanVersionId(planVersionIdRaw) {
|
|
175
|
+
const planVersionId = String(planVersionIdRaw || '').trim();
|
|
176
|
+
if (planVersionId) {
|
|
177
|
+
return this.getPlanVersionSnapshot(planVersionId).planVersionId;
|
|
178
|
+
}
|
|
179
|
+
return this.getDefaultPlanVersionByMode('personal', { allowFallback: true }).planVersionId;
|
|
180
|
+
}
|
|
181
|
+
buildBlockedAccessStatus(input) {
|
|
182
|
+
return {
|
|
183
|
+
gate: input.gate,
|
|
184
|
+
hasEntitlement: Boolean(input.entitlement),
|
|
185
|
+
canAccessApp: false,
|
|
186
|
+
canAccessPlans: true,
|
|
187
|
+
planSelectionRequired: input.planSelectionRequired === true,
|
|
188
|
+
message: input.message,
|
|
189
|
+
entitlement: input.entitlement
|
|
190
|
+
};
|
|
191
|
+
}
|
|
115
192
|
// ─── Plan defaults repair ─────────────────────────────────────────────────
|
|
116
193
|
repairPlanDefaultVersionIfNeeded(planIdRaw) {
|
|
117
194
|
const planId = String(planIdRaw || '').trim().toLowerCase();
|
|
@@ -152,42 +229,6 @@ export class PlanEntitlementService {
|
|
|
152
229
|
this.repairPlanDefaultVersionIfNeeded(planId);
|
|
153
230
|
}
|
|
154
231
|
}
|
|
155
|
-
repairSignupMonetizationSettingsIfNeeded() {
|
|
156
|
-
const current = this.deps.readGlobalConfig();
|
|
157
|
-
const currentAuth = current.auth && typeof current.auth === 'object'
|
|
158
|
-
? current.auth
|
|
159
|
-
: {};
|
|
160
|
-
const strategy = currentAuth.signupMonetizationStrategy === 'plan_selection_required'
|
|
161
|
-
? 'plan_selection_required'
|
|
162
|
-
: currentAuth.signupMonetizationStrategy === 'auto_assign_default'
|
|
163
|
-
? 'auto_assign_default'
|
|
164
|
-
: null;
|
|
165
|
-
const hasStrategy = strategy === 'auto_assign_default' || strategy === 'plan_selection_required';
|
|
166
|
-
const hasDefaultVersionId = typeof currentAuth.defaultSignupPlanVersionId === 'string'
|
|
167
|
-
&& currentAuth.defaultSignupPlanVersionId.trim().length > 0;
|
|
168
|
-
if (strategy === 'plan_selection_required') {
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (strategy === 'auto_assign_default' && hasDefaultVersionId) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const fallbackVersion = this.tryGetDefaultPlanVersionByMode('personal', { allowFallback: true });
|
|
175
|
-
if (!fallbackVersion) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
this.deps.writeGlobalConfig({
|
|
179
|
-
...current,
|
|
180
|
-
auth: {
|
|
181
|
-
...currentAuth,
|
|
182
|
-
signupMonetizationStrategy: hasStrategy
|
|
183
|
-
? strategy
|
|
184
|
-
: 'auto_assign_default',
|
|
185
|
-
defaultSignupPlanVersionId: strategy === 'auto_assign_default' && hasDefaultVersionId
|
|
186
|
-
? currentAuth.defaultSignupPlanVersionId
|
|
187
|
-
: fallbackVersion.planVersionId
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
232
|
normalizeLegacyPlanDataIfNeeded() {
|
|
192
233
|
this.migrateLegacySeatLimitFeatureConfigToVersionSeatLimit();
|
|
193
234
|
this.normalizeLegacyBillingMirrorRows();
|
|
@@ -301,16 +342,16 @@ export class PlanEntitlementService {
|
|
|
301
342
|
if (!accountId)
|
|
302
343
|
continue;
|
|
303
344
|
const rawBillingState = String(row.entitlement_state || '').trim().toLowerCase();
|
|
304
|
-
if (rawBillingState === 'pending_plan_selection')
|
|
305
|
-
continue;
|
|
306
345
|
const mirroredPlanId = String(row.tier || '').trim().toLowerCase();
|
|
307
346
|
const planVersion = this.tryGetDefaultPlanVersionByPlanId(mirroredPlanId)
|
|
308
347
|
|| defaultPersonalVersion;
|
|
309
|
-
this.
|
|
348
|
+
this.transitionCanonicalCommercialState({
|
|
310
349
|
accountId,
|
|
311
350
|
planVersionId: planVersion.planVersionId,
|
|
312
351
|
state: this.resolveCanonicalEntitlementStateFromBilling(row.entitlement_state),
|
|
313
352
|
effectiveAt: row.billing_created_at ? String(row.billing_created_at) : undefined,
|
|
353
|
+
billingMirrorPlanId: rawBillingState === 'pending_plan_selection' ? null : planVersion.planId,
|
|
354
|
+
source: 'backfill',
|
|
314
355
|
updatedBy: 'backfill',
|
|
315
356
|
createdAt: row.billing_created_at ? String(row.billing_created_at) : undefined,
|
|
316
357
|
updatedAt: row.billing_updated_at ? String(row.billing_updated_at) : undefined
|
|
@@ -351,7 +392,39 @@ export class PlanEntitlementService {
|
|
|
351
392
|
resolveCanonicalEntitlementStateFromBilling(raw) {
|
|
352
393
|
return this.normalizeEntitlementState(raw);
|
|
353
394
|
}
|
|
354
|
-
|
|
395
|
+
transitionCanonicalCommercialState(input) {
|
|
396
|
+
const accountId = String(input.accountId || '').trim();
|
|
397
|
+
if (!accountId) {
|
|
398
|
+
throw new TaskforceRuleError('accountId is required', 400, 'ACCOUNT_ID_REQUIRED');
|
|
399
|
+
}
|
|
400
|
+
const nextState = this.normalizeEntitlementState(input.state);
|
|
401
|
+
const existing = this.tryGetAccountEntitlement(accountId);
|
|
402
|
+
const planVersionId = nextState === 'pending_plan_selection'
|
|
403
|
+
? this.resolvePendingPlanVersionId(input.planVersionId || existing?.planVersionId || null)
|
|
404
|
+
: String(input.planVersionId || existing?.planVersionId || '').trim();
|
|
405
|
+
if (!planVersionId) {
|
|
406
|
+
throw new TaskforceRuleError('planVersionId is required', 400, 'PLAN_VERSION_ID_REQUIRED');
|
|
407
|
+
}
|
|
408
|
+
const planVersion = this.getPlanVersionSnapshot(planVersionId);
|
|
409
|
+
if (existing && !this.isValidEntitlementTransition(existing.state, nextState)) {
|
|
410
|
+
throw new TaskforceRuleError('Invalid entitlement state transition', 409, 'INVALID_STATE_TRANSITION');
|
|
411
|
+
}
|
|
412
|
+
const planHasActivePricing = this.planHasAnyActivePricing(planVersion.planId);
|
|
413
|
+
const planVersionHasActiveFreePricing = this.planVersionHasActiveFreePricing(planVersion.planVersionId);
|
|
414
|
+
if ((nextState === 'active' || nextState === 'grace')
|
|
415
|
+
&& planHasActivePricing
|
|
416
|
+
&& !planVersionHasActiveFreePricing
|
|
417
|
+
&& !this.isBillingConfirmedTransitionSource(input.source)
|
|
418
|
+
&& input.source !== 'admin'
|
|
419
|
+
&& input.source !== 'backfill'
|
|
420
|
+
&& input.source !== 'legacy-plan-mode') {
|
|
421
|
+
throw new TaskforceRuleError('Paid active access can only be activated from billing-confirmed transitions.', 409, 'PAID_ACCESS_REQUIRES_BILLING_CONFIRMATION', {
|
|
422
|
+
accountId,
|
|
423
|
+
planVersionId,
|
|
424
|
+
state: nextState,
|
|
425
|
+
source: input.source
|
|
426
|
+
});
|
|
427
|
+
}
|
|
355
428
|
const now = new Date().toISOString();
|
|
356
429
|
const createdAt = input.createdAt || now;
|
|
357
430
|
const updatedAt = input.updatedAt || now;
|
|
@@ -365,7 +438,21 @@ export class PlanEntitlementService {
|
|
|
365
438
|
effective_until = excluded.effective_until,
|
|
366
439
|
updated_by = excluded.updated_by,
|
|
367
440
|
updated_at = excluded.updated_at
|
|
368
|
-
`).run(
|
|
441
|
+
`).run(accountId, this.tenantId, planVersion.planVersionId, nextState, input.effectiveAt || now, input.effectiveUntil || null, input.updatedBy, createdAt, updatedAt);
|
|
442
|
+
this.createUserBillingRecord(accountId, {
|
|
443
|
+
entitlementState: nextState,
|
|
444
|
+
planId: input.billingMirrorPlanId !== undefined
|
|
445
|
+
? input.billingMirrorPlanId
|
|
446
|
+
: (nextState === 'pending_plan_selection' ? null : planVersion.planId),
|
|
447
|
+
effectiveUntil: input.effectiveUntil || null
|
|
448
|
+
});
|
|
449
|
+
return this.getAccountEntitlement(accountId);
|
|
450
|
+
}
|
|
451
|
+
upsertCanonicalEntitlementRecord(input) {
|
|
452
|
+
this.transitionCanonicalCommercialState({
|
|
453
|
+
...input,
|
|
454
|
+
source: this.resolveCanonicalTransitionSource(input.updatedBy)
|
|
455
|
+
});
|
|
369
456
|
}
|
|
370
457
|
isFeatureEnabledForPlanMode(feature) {
|
|
371
458
|
return Boolean(feature && feature.access !== 'disabled');
|
|
@@ -463,6 +550,26 @@ export class PlanEntitlementService {
|
|
|
463
550
|
`).get(this.tenantId, planId);
|
|
464
551
|
return Number(row?.count || 0) > 0;
|
|
465
552
|
}
|
|
553
|
+
planVersionHasActiveFreePricing(planVersionIdRaw) {
|
|
554
|
+
const planVersionId = String(planVersionIdRaw || '').trim();
|
|
555
|
+
if (!planVersionId)
|
|
556
|
+
return false;
|
|
557
|
+
const row = this.db.prepare(`
|
|
558
|
+
SELECT COUNT(*) AS count
|
|
559
|
+
FROM billing_price_mappings
|
|
560
|
+
WHERE tenant_id = ?
|
|
561
|
+
AND plan_version_id = ?
|
|
562
|
+
AND active = 1
|
|
563
|
+
AND pricing_type = 'free'
|
|
564
|
+
`).get(this.tenantId, planVersionId);
|
|
565
|
+
return Number(row?.count || 0) > 0;
|
|
566
|
+
}
|
|
567
|
+
planVersionIsTrialEligible(planVersionIdRaw) {
|
|
568
|
+
const planVersionId = String(planVersionIdRaw || '').trim();
|
|
569
|
+
if (!planVersionId)
|
|
570
|
+
return false;
|
|
571
|
+
return this.getPlanVersionSnapshot(planVersionId).trialDays > 0;
|
|
572
|
+
}
|
|
466
573
|
tryGetDefaultPlanVersionByPlanId(planIdRaw) {
|
|
467
574
|
const planId = String(planIdRaw || '').trim().toLowerCase();
|
|
468
575
|
if (!planId)
|
|
@@ -522,51 +629,169 @@ export class PlanEntitlementService {
|
|
|
522
629
|
throw error;
|
|
523
630
|
}
|
|
524
631
|
}
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
632
|
+
tryGetImplicitFreeOnboardingPlanVersion() {
|
|
633
|
+
const rows = this.db.prepare(`
|
|
634
|
+
SELECT DISTINCT v.plan_version_id
|
|
635
|
+
FROM plan_versions v
|
|
636
|
+
JOIN plan_catalog p
|
|
637
|
+
ON p.tenant_id = v.tenant_id
|
|
638
|
+
AND p.plan_id = v.plan_id
|
|
639
|
+
JOIN billing_price_mappings bpm
|
|
640
|
+
ON bpm.tenant_id = v.tenant_id
|
|
641
|
+
AND bpm.plan_version_id = v.plan_version_id
|
|
642
|
+
WHERE v.tenant_id = ?
|
|
643
|
+
AND COALESCE(p.status, 'active') = 'active'
|
|
644
|
+
AND bpm.active = 1
|
|
645
|
+
AND bpm.pricing_type = 'free'
|
|
646
|
+
ORDER BY v.is_default DESC, p.updated_at DESC, p.created_at DESC, v.version_number DESC, v.created_at DESC
|
|
647
|
+
`).all(this.tenantId);
|
|
648
|
+
for (const row of rows) {
|
|
649
|
+
const planVersionId = String(row.plan_version_id || '').trim();
|
|
650
|
+
if (!planVersionId)
|
|
651
|
+
continue;
|
|
652
|
+
const version = this.getPlanVersionSnapshot(planVersionId);
|
|
653
|
+
if (!this.isTeamPlanVersion(version)) {
|
|
654
|
+
return version;
|
|
537
655
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
getOnboardingPolicy() {
|
|
660
|
+
const billing = this.deps.readGlobalConfig().billing || {};
|
|
661
|
+
const policy = billing.onboardingPolicy && typeof billing.onboardingPolicy === 'object'
|
|
662
|
+
? billing.onboardingPolicy
|
|
663
|
+
: null;
|
|
664
|
+
if (policy?.mode === 'require_plan_selection'
|
|
665
|
+
|| policy?.mode === 'auto_grant_free'
|
|
666
|
+
|| policy?.mode === 'auto_start_trial') {
|
|
667
|
+
return {
|
|
668
|
+
mode: policy.mode,
|
|
669
|
+
planVersionId: typeof policy.planVersionId === 'string'
|
|
670
|
+
? (policy.planVersionId.trim() || null)
|
|
671
|
+
: null
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
const implicitFreeVersion = this.tryGetImplicitFreeOnboardingPlanVersion();
|
|
675
|
+
if (implicitFreeVersion) {
|
|
676
|
+
return {
|
|
677
|
+
mode: 'auto_grant_free',
|
|
678
|
+
planVersionId: implicitFreeVersion.planVersionId
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
mode: 'require_plan_selection',
|
|
683
|
+
planVersionId: null
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
validateOnboardingPolicy(input) {
|
|
687
|
+
const current = this.getOnboardingPolicy();
|
|
688
|
+
const mode = input?.mode === 'auto_grant_free' || input?.mode === 'auto_start_trial'
|
|
689
|
+
? input.mode
|
|
690
|
+
: input?.mode === 'require_plan_selection'
|
|
691
|
+
? 'require_plan_selection'
|
|
692
|
+
: current.mode;
|
|
693
|
+
const planVersionId = input?.planVersionId !== undefined
|
|
694
|
+
? (typeof input.planVersionId === 'string' ? input.planVersionId.trim() || null : null)
|
|
695
|
+
: current.planVersionId;
|
|
696
|
+
if (mode === 'require_plan_selection') {
|
|
697
|
+
return { valid: true };
|
|
698
|
+
}
|
|
699
|
+
if (!planVersionId) {
|
|
700
|
+
return {
|
|
701
|
+
valid: false,
|
|
702
|
+
code: 'ONBOARDING_PLAN_VERSION_REQUIRED',
|
|
703
|
+
message: 'An onboarding plan version is required for the selected onboarding mode.'
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
const row = this.db.prepare(`
|
|
707
|
+
SELECT v.plan_version_id, v.plan_id, COALESCE(p.status, 'active') AS plan_status
|
|
708
|
+
FROM plan_versions v
|
|
709
|
+
JOIN plan_catalog p ON p.tenant_id = v.tenant_id AND p.plan_id = v.plan_id
|
|
710
|
+
WHERE v.tenant_id = ? AND v.plan_version_id = ?
|
|
711
|
+
LIMIT 1
|
|
712
|
+
`).get(this.tenantId, planVersionId);
|
|
713
|
+
if (!row?.plan_version_id || !row.plan_id) {
|
|
714
|
+
return {
|
|
715
|
+
valid: false,
|
|
716
|
+
code: 'ONBOARDING_PLAN_VERSION_NOT_FOUND',
|
|
717
|
+
message: `Configured onboarding plan version "${planVersionId}" was not found.`,
|
|
718
|
+
details: { planVersionId }
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
if (String(row.plan_status || '').trim().toLowerCase() !== 'active') {
|
|
722
|
+
return {
|
|
723
|
+
valid: false,
|
|
724
|
+
code: 'ONBOARDING_PLAN_NOT_ENABLED',
|
|
725
|
+
message: `Configured onboarding plan version "${planVersionId}" belongs to a disabled plan.`,
|
|
726
|
+
details: {
|
|
727
|
+
planVersionId,
|
|
541
728
|
planId: String(row.plan_id)
|
|
542
|
-
}
|
|
543
|
-
}
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
if (mode === 'auto_grant_free' && !this.planVersionHasActiveFreePricing(planVersionId)) {
|
|
733
|
+
return {
|
|
734
|
+
valid: false,
|
|
735
|
+
code: 'ONBOARDING_PLAN_MUST_BE_FREE',
|
|
736
|
+
message: `Configured onboarding plan version "${planVersionId}" does not have active free pricing.`,
|
|
737
|
+
details: {
|
|
738
|
+
planVersionId,
|
|
739
|
+
planId: String(row.plan_id)
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
if (mode === 'auto_start_trial' && !this.planVersionIsTrialEligible(planVersionId)) {
|
|
744
|
+
return {
|
|
745
|
+
valid: false,
|
|
746
|
+
code: 'ONBOARDING_PLAN_TRIAL_REQUIRED',
|
|
747
|
+
message: `Configured onboarding plan version "${planVersionId}" is not trial eligible.`,
|
|
748
|
+
details: {
|
|
749
|
+
planVersionId,
|
|
750
|
+
planId: String(row.plan_id)
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
return { valid: true };
|
|
755
|
+
}
|
|
756
|
+
resolveRegistrationOnboardingOutcome(selectedPlanVersionIdRaw) {
|
|
757
|
+
const selectedPlanVersionId = String(selectedPlanVersionIdRaw || '').trim();
|
|
758
|
+
if (selectedPlanVersionId) {
|
|
759
|
+
const planVersion = this.getPlanVersionSnapshot(selectedPlanVersionId);
|
|
760
|
+
const commercialState = this.planVersionHasActiveFreePricing(planVersion.planVersionId)
|
|
761
|
+
? 'active'
|
|
762
|
+
: 'checkout_pending';
|
|
544
763
|
return {
|
|
545
764
|
requiresPlanSelection: false,
|
|
546
|
-
|
|
547
|
-
|
|
765
|
+
checkoutPending: commercialState === 'checkout_pending',
|
|
766
|
+
commercialState,
|
|
767
|
+
planVersionId: planVersion.planVersionId,
|
|
768
|
+
planId: planVersion.planId
|
|
548
769
|
};
|
|
549
770
|
}
|
|
550
|
-
const
|
|
551
|
-
|
|
771
|
+
const onboardingPolicy = this.getOnboardingPolicy();
|
|
772
|
+
const validation = this.validateOnboardingPolicy(onboardingPolicy);
|
|
773
|
+
if (!validation.valid) {
|
|
774
|
+
throw new TaskforceRuleError(validation.message || 'Onboarding policy is misconfigured.', 500, validation.code || 'ONBOARDING_POLICY_INVALID', validation.details);
|
|
775
|
+
}
|
|
776
|
+
if (onboardingPolicy.mode === 'require_plan_selection') {
|
|
552
777
|
return {
|
|
553
778
|
requiresPlanSelection: true,
|
|
779
|
+
checkoutPending: false,
|
|
780
|
+
commercialState: 'pending_plan_selection',
|
|
554
781
|
planVersionId: null,
|
|
555
782
|
planId: null
|
|
556
783
|
};
|
|
557
784
|
}
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
throw new TaskforceRuleError(validation.message || 'Signup monetization is misconfigured.', 500, validation.code || 'SIGNUP_MONETIZATION_MISCONFIGURED', validation.details);
|
|
561
|
-
}
|
|
562
|
-
const configuredVersion = signupSettings.strategy === 'auto_assign_default' && signupSettings.defaultSignupPlanVersionId
|
|
563
|
-
? this.getPlanVersionSnapshot(signupSettings.defaultSignupPlanVersionId)
|
|
785
|
+
const configuredVersion = onboardingPolicy.planVersionId
|
|
786
|
+
? this.getPlanVersionSnapshot(onboardingPolicy.planVersionId)
|
|
564
787
|
: null;
|
|
565
788
|
if (!configuredVersion) {
|
|
566
|
-
throw new TaskforceRuleError('
|
|
789
|
+
throw new TaskforceRuleError('Configured onboarding plan version is required.', 500, 'ONBOARDING_PLAN_VERSION_REQUIRED');
|
|
567
790
|
}
|
|
568
791
|
return {
|
|
569
792
|
requiresPlanSelection: false,
|
|
793
|
+
checkoutPending: false,
|
|
794
|
+
commercialState: onboardingPolicy.mode === 'auto_start_trial' ? 'trialing' : 'active',
|
|
570
795
|
planVersionId: configuredVersion.planVersionId,
|
|
571
796
|
planId: configuredVersion.planId
|
|
572
797
|
};
|
|
@@ -578,14 +803,22 @@ export class PlanEntitlementService {
|
|
|
578
803
|
const planId = typeof options?.planId === 'string'
|
|
579
804
|
? options.planId.trim().toLowerCase()
|
|
580
805
|
: '';
|
|
806
|
+
const shouldUpdateEffectiveUntil = options ? Object.prototype.hasOwnProperty.call(options, 'effectiveUntil') : false;
|
|
807
|
+
const effectiveUntil = shouldUpdateEffectiveUntil
|
|
808
|
+
? (typeof options?.effectiveUntil === 'string' ? options.effectiveUntil : null)
|
|
809
|
+
: null;
|
|
581
810
|
this.db.prepare(`
|
|
582
|
-
INSERT INTO user_billing (user_id, tenant_id, entitlement_state, tier, created_at, updated_at)
|
|
583
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
811
|
+
INSERT INTO user_billing (user_id, tenant_id, entitlement_state, tier, effective_until, created_at, updated_at)
|
|
812
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
584
813
|
ON CONFLICT (user_id) DO UPDATE SET
|
|
585
814
|
entitlement_state = excluded.entitlement_state,
|
|
586
815
|
tier = excluded.tier,
|
|
816
|
+
effective_until = CASE
|
|
817
|
+
WHEN ? THEN excluded.effective_until
|
|
818
|
+
ELSE user_billing.effective_until
|
|
819
|
+
END,
|
|
587
820
|
updated_at = excluded.updated_at
|
|
588
|
-
`).run(userId, this.tenantId, entitlementState, planId, now, now);
|
|
821
|
+
`).run(userId, this.tenantId, entitlementState, planId, effectiveUntil, now, now, shouldUpdateEffectiveUntil ? 1 : 0);
|
|
589
822
|
}
|
|
590
823
|
getUserEntitlement(userId) {
|
|
591
824
|
const account = this.getAccountEntitlement(userId);
|
|
@@ -616,88 +849,55 @@ export class PlanEntitlementService {
|
|
|
616
849
|
if (!normalizedAccountId) {
|
|
617
850
|
throw new TaskforceRuleError('accountId is required', 400, 'ACCOUNT_ID_REQUIRED');
|
|
618
851
|
}
|
|
619
|
-
const
|
|
620
|
-
const validation = this.
|
|
852
|
+
const onboardingPolicy = this.getOnboardingPolicy();
|
|
853
|
+
const validation = this.validateOnboardingPolicy(onboardingPolicy);
|
|
621
854
|
const entitlement = this.tryGetAccountEntitlement(normalizedAccountId);
|
|
622
|
-
if (entitlement
|
|
855
|
+
if (!entitlement) {
|
|
856
|
+
return !validation.valid
|
|
857
|
+
? this.buildBlockedAccessStatus({
|
|
858
|
+
gate: 'misconfigured',
|
|
859
|
+
message: validation.message || 'Onboarding policy is misconfigured.',
|
|
860
|
+
entitlement: null
|
|
861
|
+
})
|
|
862
|
+
: this.buildBlockedAccessStatus({
|
|
863
|
+
gate: 'missing_entitlement',
|
|
864
|
+
message: 'No entitlement is linked to this account.',
|
|
865
|
+
entitlement: null
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
if (this.isEntitlementStateAllowed(entitlement.state)) {
|
|
623
869
|
return {
|
|
624
870
|
gate: 'ok',
|
|
625
871
|
hasEntitlement: true,
|
|
626
872
|
canAccessApp: true,
|
|
627
873
|
canAccessPlans: true,
|
|
628
|
-
signupMonetizationStrategy: signupSettings.strategy,
|
|
629
874
|
planSelectionRequired: false,
|
|
630
875
|
message: null,
|
|
631
876
|
entitlement
|
|
632
877
|
};
|
|
633
878
|
}
|
|
634
|
-
if (entitlement) {
|
|
635
|
-
return {
|
|
636
|
-
gate: 'missing_entitlement',
|
|
637
|
-
hasEntitlement: true,
|
|
638
|
-
canAccessApp: false,
|
|
639
|
-
canAccessPlans: true,
|
|
640
|
-
signupMonetizationStrategy: signupSettings.strategy,
|
|
641
|
-
planSelectionRequired: false,
|
|
642
|
-
message: entitlement.state === 'suspended'
|
|
643
|
-
? 'Your subscription is suspended. Update billing to continue.'
|
|
644
|
-
: 'Your plan is inactive. Choose a plan to continue.',
|
|
645
|
-
entitlement
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
if (signupSettings.strategy === 'plan_selection_required') {
|
|
649
|
-
const billingRow = this.db.prepare(`
|
|
650
|
-
SELECT entitlement_state
|
|
651
|
-
FROM user_billing
|
|
652
|
-
WHERE tenant_id = ? AND user_id = ?
|
|
653
|
-
LIMIT 1
|
|
654
|
-
`).get(this.tenantId, normalizedAccountId);
|
|
655
|
-
const billingState = String(billingRow?.entitlement_state || '').trim().toLowerCase();
|
|
656
|
-
if (billingState === 'pending_plan_selection') {
|
|
657
|
-
return {
|
|
658
|
-
gate: 'plan_selection_required',
|
|
659
|
-
hasEntitlement: false,
|
|
660
|
-
canAccessApp: false,
|
|
661
|
-
canAccessPlans: true,
|
|
662
|
-
signupMonetizationStrategy: signupSettings.strategy,
|
|
663
|
-
planSelectionRequired: true,
|
|
664
|
-
message: 'Choose a plan to continue.',
|
|
665
|
-
entitlement: null
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
return {
|
|
879
|
+
if (entitlement.state === 'pending_plan_selection') {
|
|
880
|
+
return this.buildBlockedAccessStatus({
|
|
669
881
|
gate: 'plan_selection_required',
|
|
670
|
-
hasEntitlement: false,
|
|
671
|
-
canAccessApp: false,
|
|
672
|
-
canAccessPlans: true,
|
|
673
|
-
signupMonetizationStrategy: signupSettings.strategy,
|
|
674
|
-
planSelectionRequired: true,
|
|
675
882
|
message: 'Choose a plan to continue.',
|
|
676
|
-
entitlement
|
|
677
|
-
|
|
883
|
+
entitlement,
|
|
884
|
+
planSelectionRequired: true
|
|
885
|
+
});
|
|
678
886
|
}
|
|
679
|
-
if (
|
|
680
|
-
return {
|
|
681
|
-
gate: '
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
signupMonetizationStrategy: signupSettings.strategy,
|
|
686
|
-
planSelectionRequired: false,
|
|
687
|
-
message: validation.message || 'Signup monetization is misconfigured.',
|
|
688
|
-
entitlement: null
|
|
689
|
-
};
|
|
887
|
+
if (entitlement.state === 'checkout_pending') {
|
|
888
|
+
return this.buildBlockedAccessStatus({
|
|
889
|
+
gate: 'checkout_pending',
|
|
890
|
+
message: 'Complete checkout to continue.',
|
|
891
|
+
entitlement
|
|
892
|
+
});
|
|
690
893
|
}
|
|
691
|
-
return {
|
|
894
|
+
return this.buildBlockedAccessStatus({
|
|
692
895
|
gate: 'missing_entitlement',
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
message: 'No entitlement is linked to this account.',
|
|
699
|
-
entitlement: null
|
|
700
|
-
};
|
|
896
|
+
message: entitlement.state === 'suspended'
|
|
897
|
+
? 'Your subscription is suspended. Update billing to continue.'
|
|
898
|
+
: 'Your plan is inactive. Choose a plan to continue.',
|
|
899
|
+
entitlement
|
|
900
|
+
});
|
|
701
901
|
}
|
|
702
902
|
getEffectivePlanVersion(accountId) {
|
|
703
903
|
const normalizedAccountId = String(accountId || '').trim();
|
|
@@ -749,12 +949,7 @@ export class PlanEntitlementService {
|
|
|
749
949
|
if (!normalizedAccountId) {
|
|
750
950
|
throw new TaskforceRuleError('accountId is required', 400, 'ACCOUNT_ID_REQUIRED');
|
|
751
951
|
}
|
|
752
|
-
const row = this.
|
|
753
|
-
SELECT account_id, plan_version_id, entitlement_state, effective_at, effective_until
|
|
754
|
-
FROM account_entitlements
|
|
755
|
-
WHERE tenant_id = ? AND account_id = ?
|
|
756
|
-
LIMIT 1
|
|
757
|
-
`).get(this.tenantId, normalizedAccountId);
|
|
952
|
+
const row = this.normalizeExpiredCheckoutPendingRow(normalizedAccountId);
|
|
758
953
|
if (!row?.account_id || !row.plan_version_id) {
|
|
759
954
|
throw new TaskforceRuleError('Entitlement not found', 404, 'ENTITLEMENT_NOT_FOUND');
|
|
760
955
|
}
|
|
@@ -847,25 +1042,15 @@ export class PlanEntitlementService {
|
|
|
847
1042
|
}
|
|
848
1043
|
const normalizedMode = String(mode || '').trim().toLowerCase() === 'team' ? 'team' : 'personal';
|
|
849
1044
|
const targetVersion = this.getDefaultPlanVersionByMode(normalizedMode);
|
|
850
|
-
const now = new Date().toISOString();
|
|
851
|
-
this.db.prepare(`
|
|
852
|
-
INSERT INTO user_billing (user_id, tenant_id, entitlement_state, tier, created_at, updated_at)
|
|
853
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
854
|
-
ON CONFLICT (user_id) DO UPDATE SET
|
|
855
|
-
entitlement_state = excluded.entitlement_state,
|
|
856
|
-
tier = excluded.tier,
|
|
857
|
-
updated_at = excluded.updated_at
|
|
858
|
-
`).run(normalizedUserId, this.tenantId, 'active', targetVersion.planId, now, now);
|
|
859
|
-
const planVersionId = targetVersion.planVersionId;
|
|
860
1045
|
const entitlementState = 'active';
|
|
861
|
-
this.
|
|
1046
|
+
this.transitionCanonicalCommercialState({
|
|
862
1047
|
accountId: normalizedUserId,
|
|
863
|
-
planVersionId,
|
|
1048
|
+
planVersionId: targetVersion.planVersionId,
|
|
864
1049
|
state: entitlementState,
|
|
865
|
-
effectiveAt:
|
|
1050
|
+
effectiveAt: new Date().toISOString(),
|
|
1051
|
+
billingMirrorPlanId: targetVersion.planId,
|
|
1052
|
+
source: 'legacy-plan-mode',
|
|
866
1053
|
updatedBy: 'legacy-plan-mode',
|
|
867
|
-
createdAt: now,
|
|
868
|
-
updatedAt: now
|
|
869
1054
|
});
|
|
870
1055
|
return normalizedMode;
|
|
871
1056
|
}
|
|
@@ -882,76 +1067,18 @@ export class PlanEntitlementService {
|
|
|
882
1067
|
entitlementState: 'active'
|
|
883
1068
|
};
|
|
884
1069
|
}
|
|
885
|
-
// ─── Signup monetization settings ─────────────────────────────────────────
|
|
886
|
-
getSignupMonetizationSettings() {
|
|
887
|
-
const auth = this.deps.readGlobalConfig().auth || {};
|
|
888
|
-
return {
|
|
889
|
-
strategy: auth.signupMonetizationStrategy === 'plan_selection_required'
|
|
890
|
-
? 'plan_selection_required'
|
|
891
|
-
: 'auto_assign_default',
|
|
892
|
-
defaultSignupPlanVersionId: typeof auth.defaultSignupPlanVersionId === 'string'
|
|
893
|
-
? (auth.defaultSignupPlanVersionId.trim() || null)
|
|
894
|
-
: null
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
validateSignupMonetizationSettings(input) {
|
|
898
|
-
const current = this.getSignupMonetizationSettings();
|
|
899
|
-
const strategy = input?.strategy === 'plan_selection_required'
|
|
900
|
-
? 'plan_selection_required'
|
|
901
|
-
: input?.strategy === 'auto_assign_default'
|
|
902
|
-
? 'auto_assign_default'
|
|
903
|
-
: current.strategy;
|
|
904
|
-
const defaultSignupPlanVersionId = input?.defaultSignupPlanVersionId !== undefined
|
|
905
|
-
? (typeof input.defaultSignupPlanVersionId === 'string' ? input.defaultSignupPlanVersionId.trim() || null : null)
|
|
906
|
-
: current.defaultSignupPlanVersionId;
|
|
907
|
-
if (strategy === 'plan_selection_required') {
|
|
908
|
-
return { valid: true };
|
|
909
|
-
}
|
|
910
|
-
if (!defaultSignupPlanVersionId) {
|
|
911
|
-
return {
|
|
912
|
-
valid: false,
|
|
913
|
-
code: 'SIGNUP_PLAN_VERSION_REQUIRED',
|
|
914
|
-
message: 'Default signup plan version is required when signup strategy is auto-assign default.'
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
const row = this.db.prepare(`
|
|
918
|
-
SELECT v.plan_version_id, v.plan_id, COALESCE(p.status, 'active') AS plan_status
|
|
919
|
-
FROM plan_versions v
|
|
920
|
-
JOIN plan_catalog p ON p.tenant_id = v.tenant_id AND p.plan_id = v.plan_id
|
|
921
|
-
WHERE v.tenant_id = ? AND v.plan_version_id = ?
|
|
922
|
-
LIMIT 1
|
|
923
|
-
`).get(this.tenantId, defaultSignupPlanVersionId);
|
|
924
|
-
if (!row?.plan_version_id || !row.plan_id) {
|
|
925
|
-
return {
|
|
926
|
-
valid: false,
|
|
927
|
-
code: 'SIGNUP_PLAN_VERSION_NOT_FOUND',
|
|
928
|
-
message: `Configured default signup plan version "${defaultSignupPlanVersionId}" was not found.`,
|
|
929
|
-
details: { defaultSignupPlanVersionId }
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
if (String(row.plan_status || '').trim().toLowerCase() !== 'active') {
|
|
933
|
-
return {
|
|
934
|
-
valid: false,
|
|
935
|
-
code: 'SIGNUP_PLAN_NOT_ENABLED',
|
|
936
|
-
message: `Configured default signup plan version "${defaultSignupPlanVersionId}" belongs to a disabled plan.`,
|
|
937
|
-
details: {
|
|
938
|
-
defaultSignupPlanVersionId,
|
|
939
|
-
planId: String(row.plan_id)
|
|
940
|
-
}
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
return { valid: true };
|
|
944
|
-
}
|
|
945
1070
|
// ─── Plan catalog admin ───────────────────────────────────────────────────
|
|
946
1071
|
isValidEntitlementTransition(fromState, toState) {
|
|
947
1072
|
if (fromState === toState)
|
|
948
1073
|
return true;
|
|
949
1074
|
const allowed = {
|
|
1075
|
+
pending_plan_selection: ['checkout_pending', 'trialing', 'active', 'canceled'],
|
|
1076
|
+
checkout_pending: ['pending_plan_selection', 'active', 'trialing', 'canceled'],
|
|
950
1077
|
trialing: ['active', 'grace', 'canceled'],
|
|
951
1078
|
active: ['grace', 'suspended', 'canceled'],
|
|
952
1079
|
grace: ['active', 'suspended', 'canceled'],
|
|
953
1080
|
suspended: ['active', 'canceled'],
|
|
954
|
-
canceled: ['trialing', 'active']
|
|
1081
|
+
canceled: ['pending_plan_selection', 'checkout_pending', 'trialing', 'active']
|
|
955
1082
|
};
|
|
956
1083
|
return allowed[fromState].includes(toState);
|
|
957
1084
|
}
|
|
@@ -1358,12 +1485,13 @@ export class PlanEntitlementService {
|
|
|
1358
1485
|
const effectiveAt = input.effectiveAt === undefined ? (before?.effectiveAt || null) : (input.effectiveAt ? String(input.effectiveAt) : null);
|
|
1359
1486
|
const effectiveUntil = input.effectiveUntil === undefined ? (before?.effectiveUntil || null) : (input.effectiveUntil ? String(input.effectiveUntil) : null);
|
|
1360
1487
|
const now = new Date().toISOString();
|
|
1361
|
-
this.
|
|
1488
|
+
this.transitionCanonicalCommercialState({
|
|
1362
1489
|
accountId,
|
|
1363
1490
|
planVersionId: nextPlanVersionId,
|
|
1364
1491
|
state: nextState,
|
|
1365
1492
|
effectiveAt,
|
|
1366
1493
|
effectiveUntil,
|
|
1494
|
+
source: 'admin',
|
|
1367
1495
|
updatedBy: String(input.actorUserId || 'system').trim() || 'system',
|
|
1368
1496
|
createdAt: before?.effectiveAt || now,
|
|
1369
1497
|
updatedAt: now
|