1- // ─── TEOS AI Engine — Single Source of Truth ─────────────────────────────────
2- // All plans, prices, limits, and Dodo links live here.
3- // NEVER hardcode these anywhere else in the codebase.
4- // DO NOT change Dodo links without updating this file.
5-
6- import { isAdminEmail } from "./access" ;
1+ // ─── Single source of truth for all TEOS plans ───────────────────────────────
72
83export type PlanId =
94 | "free"
@@ -17,221 +12,93 @@ export type PlanId =
1712export interface Plan {
1813 id : PlanId ;
1914 name : string ;
20- label : string ;
15+ badge : string ;
2116 price : string ;
2217 period : string ;
23- dodoPLink : string | null ;
24- isLifetime : boolean ;
25- isAgency : boolean ;
26- dailyPostLimit : number ;
27- totalPostLimit : number ;
18+ dailyLimit : number ;
19+ totalLimit : number ;
20+ platforms : number ;
2821 teamSeats : number ;
29- badge : string | null ;
22+ dodLink : string | null ;
3023 features : string [ ] ;
31- color : "gold" | "purple" | "white" ;
32- platforms : number ;
24+ highlight : boolean ;
25+ color : string ;
3326}
3427
3528export const PLANS : Record < PlanId , Plan > = {
3629 free : {
37- id : "free" ,
38- name : "Starter" ,
39- label : "Starter" ,
40- price : "$0" ,
41- period : "free forever" ,
42- dodoPLink : null ,
43- isLifetime : false ,
44- isAgency : false ,
45- dailyPostLimit : 5 ,
46- totalPostLimit : 5 ,
47- teamSeats : 1 ,
48- badge : null ,
49- platforms : 2 ,
50- features : [
51- "5 posts total" ,
52- "1 platform" ,
53- "Basic visibility score" ,
54- "No credit card" ,
55- ] ,
56- color : "white" ,
30+ id : "free" , name : "Starter" , badge : "Free" , price : "$0" , period : "free" ,
31+ dailyLimit : 5 , totalLimit : 5 , platforms : 2 , teamSeats : 1 , dodLink : null ,
32+ features : [ "5 posts total" , "X + LinkedIn only" , "Basic visibility score" , "No credit card" ] ,
33+ highlight : false , color : "#888" ,
5734 } ,
5835 pro_monthly : {
59- id : "pro_monthly" ,
60- name : "Pro Monthly" ,
61- label : "Pro" ,
62- price : "$29" ,
63- period : "/month" ,
64- dodoPLink : "https://dodo.pe/ljkagv2ixcr" ,
65- isLifetime : false ,
66- isAgency : false ,
67- dailyPostLimit : 50 ,
68- totalPostLimit : - 1 ,
69- teamSeats : 1 ,
70- badge : "Most Popular" ,
71- platforms : 7 ,
72- features : [
73- "50 posts per day" ,
74- "All 7 platforms" ,
75- "Full visibility scoring" ,
76- "CTA suggestions" ,
77- "Arabic content mode" ,
78- "Cancel anytime" ,
79- ] ,
80- color : "purple" ,
36+ id : "pro_monthly" , name : "Pro Monthly" , badge : "Pro" , price : "$29" , period : "/month" ,
37+ dailyLimit : - 1 , totalLimit : - 1 , platforms : 7 , teamSeats : 1 , dodLink : null ,
38+ features : [ "Unlimited posts" , "All 7 platforms" , "Advanced metrics" ] ,
39+ highlight : false , color : "#333" ,
8140 } ,
8241 agency_monthly : {
83- id : "agency_monthly" ,
84- name : "Agency Monthly" ,
85- label : "Agency" ,
86- price : "$69" ,
87- period : "/month" ,
88- dodoPLink : "https://dodo.pe/dbvnd9a4pp" ,
89- isLifetime : false ,
90- isAgency : true ,
91- dailyPostLimit : 200 ,
92- totalPostLimit : - 1 ,
93- teamSeats : 5 ,
94- badge : null ,
95- platforms : 7 ,
96- features : [
97- "200 posts per day" ,
98- "All 7 platforms" ,
99- "5 team seats" ,
100- "Multi-brand workspace" ,
101- "Batch generation" ,
102- "Priority support" ,
103- ] ,
104- color : "white" ,
42+ id : "agency_monthly" , name : "Agency Monthly" , badge : "Agency" , price : "$99" , period : "/month" ,
43+ dailyLimit : - 1 , totalLimit : - 1 , platforms : 7 , teamSeats : 5 , dodLink : null ,
44+ features : [ "Unlimited posts" , "5 team seats" , "Priority priority" ] ,
45+ highlight : false , color : "#444" ,
10546 } ,
10647 pro_yearly : {
107- id : "pro_yearly" ,
108- name : "Pro Yearly" ,
109- label : "Pro" ,
110- price : "$290" ,
111- period : "/year" ,
112- dodoPLink : "https://dodo.pe/ep9cgmojbua" ,
113- isLifetime : false ,
114- isAgency : false ,
115- dailyPostLimit : 50 ,
116- totalPostLimit : - 1 ,
117- teamSeats : 1 ,
118- badge : "Save $58" ,
119- platforms : 7 ,
120- features : [
121- "Everything in Pro Monthly" ,
122- "2 months free" ,
123- "All 7 platforms" ,
124- "Full visibility scoring" ,
125- "Arabic content mode" ,
126- ] ,
127- color : "purple" ,
48+ id : "pro_yearly" , name : "Pro Yearly" , badge : "Pro Annual" , price : "$249" , period : "/year" ,
49+ dailyLimit : - 1 , totalLimit : - 1 , platforms : 7 , teamSeats : 1 , dodLink : null ,
50+ features : [ "Unlimited posts" , "Save money yearly" ] ,
51+ highlight : false , color : "#555" ,
12852 } ,
12953 agency_yearly : {
130- id : "agency_yearly" ,
131- name : "Agency Yearly" ,
132- label : "Agency" ,
133- price : "$690" ,
134- period : "/year" ,
135- dodoPLink : "https://dodo.pe/79q4irl1347" ,
136- isLifetime : false ,
137- isAgency : true ,
138- dailyPostLimit : 200 ,
139- totalPostLimit : - 1 ,
140- teamSeats : 5 ,
141- badge : "Save $138" ,
142- platforms : 7 ,
143- features : [
144- "Everything in Agency Monthly" ,
145- "2 months free" ,
146- "5 team seats" ,
147- "Multi-brand workspace" ,
148- ] ,
149- color : "white" ,
54+ id : "agency_yearly" , name : "Agency Yearly" , badge : "Agency Annual" , price : "$799" , period : "/year" ,
55+ dailyLimit : - 1 , totalLimit : - 1 , platforms : 7 , teamSeats : 5 , dodLink : null ,
56+ features : [ "Unlimited posts" , "Team scale yearly" ] ,
57+ highlight : false , color : "#666" ,
15058 } ,
15159 pro_lifetime : {
152- id : "pro_lifetime" ,
153- name : "Pro Lifetime" ,
154- label : "Pro Lifetime" ,
155- price : "$149" ,
156- period : "one-time" ,
157- dodoPLink : "https://dodo.pe/relh2gradr9" ,
158- isLifetime : true ,
159- isAgency : false ,
160- dailyPostLimit : - 1 ,
161- totalPostLimit : - 1 ,
162- teamSeats : 1 ,
163- badge : "🔥 Best Value" ,
164- platforms : 7 ,
165- features : [
166- "Everything in Pro — forever" ,
167- "All future upgrades included" ,
168- "TikTok + AI video — next upgrade" ,
169- "Image generation — next upgrade" ,
170- "Priority support forever" ,
171- ] ,
172- color : "gold" ,
60+ id : "pro_lifetime" , name : "Pro Lifetime" , badge : "Pro Lifetime" , price : "$149" , period : "one-time" ,
61+ dailyLimit : - 1 , totalLimit : - 1 , platforms : 7 , teamSeats : 1 , dodLink : null ,
62+ features : [ "Unlimited forever" , "Single seat" ] ,
63+ highlight : false , color : "#777" ,
17364 } ,
17465 agency_lifetime : {
175- id : "agency_lifetime" ,
176- name : "Agency Lifetime" ,
177- label : "Agency Lifetime" ,
178- price : "$349" ,
179- period : "one-time" ,
180- dodoPLink : "https://dodo.pe/91zcmc4xi27" ,
181- isLifetime : true ,
182- isAgency : true ,
183- dailyPostLimit : - 1 ,
184- totalPostLimit : - 1 ,
185- teamSeats : 5 ,
186- badge : "🚀 Agencies" ,
187- platforms : 7 ,
188- features : [
189- "Everything in Agency — forever" ,
190- "5 team seats forever" ,
191- "White-label ready" ,
192- "TikTok + video upgrades included" ,
193- "Image generation included" ,
194- "Priority support + onboarding" ,
195- ] ,
196- color : "purple" ,
66+ id : "agency_lifetime" , name : "Founder Lifetime" , badge : "Founder" , price : "$349" , period : "one-time" ,
67+ dailyLimit : - 1 , totalLimit : - 1 , platforms : 7 , teamSeats : 5 , dodLink : null ,
68+ features : [ "Everything in Engine — forever" , "5 team seats forever" , "Predictive scoring enabled" ] ,
69+ highlight : true , color : "#C9A84C" ,
19770 } ,
19871} ;
19972
20073export function getPlan ( id : string | null | undefined ) : Plan {
20174 if ( ! id ) return PLANS . free ;
20275 const normalized = id . toLowerCase ( ) . trim ( ) ;
203- if ( normalized === "founder" || normalized === "admin" ) {
76+ if ( normalized === "founder" || normalized === "admin" || normalized === "founder lifetime" ) {
20477 return PLANS . agency_lifetime ;
20578 }
20679 if ( id in PLANS ) return PLANS [ id as PlanId ] ;
20780 return PLANS . free ;
20881}
20982
210- export function dailyLimitLabel ( plan : Plan ) : string {
211- if ( plan . dailyPostLimit === - 1 ) return "Unlimited" ;
212- return `${ plan . dailyPostLimit } posts/day` ;
213- }
83+ export function isUnlimited ( plan : Plan ) { return plan . dailyLimit === - 1 ; }
84+
85+ export function isLifetime ( plan : Plan ) { return plan . id === "pro_lifetime" || plan . id === "agency_lifetime" ; }
86+
87+ export function isPaid ( plan : Plan ) { return plan . id !== "free" ; }
21488
215- export function isWithinDailyLimit ( plan : Plan , dailyUsed : number ) : boolean {
216- if ( plan . dailyPostLimit === - 1 ) return true ;
217- return dailyUsed < plan . dailyPostLimit ;
89+ export function postsRemainingToday ( plan : Plan , dailyUsed : number ) : number | null {
90+ if ( plan . dailyLimit === - 1 ) return null ;
91+ return Math . max ( 0 , plan . dailyLimit - dailyUsed ) ;
21892}
21993
220- export function isWithinTotalLimit ( plan : Plan , totalUsed : number ) : boolean {
221- if ( plan . totalPostLimit === - 1 ) return true ;
222- return totalUsed < plan . totalPostLimit ;
94+ export function usagePct ( plan : Plan , dailyUsed : number ) : number {
95+ if ( plan . dailyLimit === - 1 ) return 0 ;
96+ return Math . min ( 100 , Math . round ( ( dailyUsed / plan . dailyLimit ) * 100 ) ) ;
22397}
22498
225- export function getUpgradePlan ( currentPlanId : PlanId ) : Plan | null {
226- const upgrades : Partial < Record < PlanId , PlanId > > = {
227- free : "pro_lifetime" ,
228- pro_monthly : "pro_lifetime" ,
229- agency_monthly : "agency_lifetime" ,
230- pro_yearly : "pro_lifetime" ,
231- agency_yearly : "agency_lifetime" ,
232- } ;
233- const next = upgrades [ currentPlanId ] ;
234- return next ? PLANS [ next ] : null ;
99+ export function upgradeTarget ( id : PlanId ) : Plan | null {
100+ if ( id === "free" ) return PLANS . pro_lifetime ;
101+ return null ;
235102}
236103
237104export const FOUNDER_EMAILS = [
@@ -243,39 +110,3 @@ export function isFounder(email: string | null | undefined): boolean {
243110 if ( ! email ) return false ;
244111 return ( FOUNDER_EMAILS as readonly string [ ] ) . includes ( email . toLowerCase ( ) ) ;
245112}
246-
247- export function isUnlimited ( plan : Plan ) { return plan . dailyPostLimit === - 1 ; }
248-
249- export function postsRemainingToday ( plan : Plan , dailyUsed : number ) : number | null {
250- if ( plan . dailyPostLimit === - 1 ) return null ;
251- return Math . max ( 0 , plan . dailyPostLimit - dailyUsed ) ;
252- }
253-
254- export function usagePct ( plan : Plan , dailyUsed : number ) : number {
255- if ( plan . dailyPostLimit === - 1 ) return 0 ;
256- return Math . min ( 100 , Math . round ( ( dailyUsed / plan . dailyPostLimit ) * 100 ) ) ;
257- }
258-
259- export function upgradeTarget ( id : PlanId ) : Plan | null {
260- const map : Partial < Record < PlanId , PlanId > > = {
261- free : "pro_lifetime" ,
262- pro_monthly : "pro_lifetime" ,
263- agency_monthly : "agency_lifetime" ,
264- pro_yearly : "pro_lifetime" ,
265- agency_yearly : "agency_lifetime" ,
266- } ;
267- const t = map [ id ] ;
268- return t ? PLANS [ t ] : null ;
269- }
270-
271- export const PLATFORM_LABELS : Record < string , string > = {
272- x : "X (Twitter)" ,
273- facebook : "Facebook" ,
274- instagram : "Instagram" ,
275- linkedin : "LinkedIn" ,
276- } ;
277-
278- export function trialExpired ( trialEndsAt : Date | string | null ) : boolean {
279- if ( ! trialEndsAt ) return true ;
280- return new Date ( ) > new Date ( trialEndsAt ) ;
281- }
0 commit comments