Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/index.cgi
Diff

/var/www/vhosts/3dshawn.com/crm.3dshawn.com/index.cgi

added on local at 2026-07-01 15:02:47

Added
+303
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 483dd3102c6f
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# ContactForge - Customer Relationship Platform landing page
4# Page body lives in TEMPLATES/cf_index.html
5#
6# Pricing block on this page pulls from billing_plans so it stays in
7# lockstep with /pricing.cgi and the admin Packages console. Falls
8# back to a built-in 4-tier list if the billing schema hasn't been
9# migrated yet.
10#
11# Also dispatches the small marketing pages via SCRIPT_NAME (Group 6
12# merge, 2026-07):
13# /about.cgi /contact.cgi /faq.cgi /features.cgi
14# /help.cgi /privacy.cgi /terms.cgi /why.cgi
15# Each of those .cgi files is a 3-line wrapper that `do`s this file.
16#======================================================================
17use strict;
18use warnings;
19
20use lib '/var/www/vhosts/3dshawn.com/crm.3dshawn.com';
21use CGI;
22use MODS::Template;
23use MODS::DBConnect;
24use MODS::Login;
25use MODS::ContactForge::Config;
26use MODS::ContactForge::Wrapper;
27use MODS::ContactForge::Billing;
28
29use MODS::ContactForge::NoOp;
30my $q = CGI->new;
31my $form = $q->Vars;
32my $auth = MODS::Login->new;
33my $wrap = MODS::ContactForge::Wrapper->new;
34my $tfile = MODS::Template->new;
35my $db = MODS::DBConnect->new;
36my $cfg = MODS::ContactForge::Config->new;
37my $bill = MODS::ContactForge::Billing->new;
38my $DB = $cfg->settings('database_name');
39
40$|=1;
41my $userinfo = $auth->login_verify(); # may be undef on marketing pages
42
43my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'index';
44
45if ($entry eq 'about') { _mkt_render_about($userinfo); exit; }
46if ($entry eq 'contact') { _mkt_render_contact($userinfo); exit; }
47if ($entry eq 'faq') { _mkt_render_faq($userinfo); exit; }
48if ($entry eq 'features') { _mkt_render_features($userinfo); exit; }
49if ($entry eq 'help') { _mkt_render_help($userinfo); exit; }
50if ($entry eq 'privacy') { _mkt_render_privacy($userinfo); exit; }
51if ($entry eq 'terms') { _mkt_render_terms($userinfo); exit; }
52if ($entry eq 'why') { _mkt_render_why($userinfo); exit; }
53
54my $dbh = $db->db_connect();
55my @plans = _build_landing_plan_cards($db, $dbh, $DB, $bill);
56
57# Active plan promo for the marketing banner above the pricing block.
58my $promo_obj = MODS::ContactForge::NoOp->new;
59my $active_promo = $promo_obj->active_plan_promo($db, $dbh, $DB);
60my $promo_code = ($active_promo && $active_promo->{code}) ? $active_promo->{code} : '';
61my $promo_label = $active_promo ? $promo_obj->marketing_label($active_promo) : '';
62
63# Per-card eligibility badge (same logic as /pricing.cgi). Cards the
64# active promo doesn't cover (free + plans outside the eligibility
65# set) simply don't render the promo line.
66if ($active_promo) {
67 my @eligible = $promo_obj->eligible_plan_ids($db, $dbh, $DB, $active_promo->{id});
68 my %eligible_set = map { $_ => 1 } @eligible;
69 my $all_paid = !scalar(@eligible);
70 foreach my $c (@plans) {
71 next if ($c->{price_display} || '$0') eq '$0';
72 next unless $all_paid || $eligible_set{ $c->{id} || 0 };
73 $c->{promo_active} = 1;
74 $c->{promo_code} = $promo_code;
75 $c->{promo_label} = $promo_label;
76 $c->{promo_card_line} = $promo_label . ' with code <strong>' . $promo_code . '</strong>';
77 }
78}
79
80$db->db_disconnect($dbh);
81
82print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
83
84my $tvars = {
85 user_id => $userinfo ? $userinfo->{user_id} : '',
86 plans => \@plans,
87 has_plans => scalar(@plans) ? 1 : 0,
88 has_promo => $active_promo ? 1 : 0,
89 promo_code => $promo_code,
90 promo_label => $promo_label,
91};
92
93my $body = join('', $tfile->template('cf_index.html', $tvars, $userinfo));
94
95$wrap->render({
96 userinfo => $userinfo,
97 title => 'Every customer touchpoint in one shared CRM',
98 body => $body,
99});
100
101#----------------------------------------------------------------------
102# Compact card shape used by the landing pricing grid (4 cards, one
103# per tier, monthly price). Bullets are pre-rendered to side-step the
104# template engine's nested-loop quirks.
105sub _build_landing_plan_cards {
106 my ($db, $dbh, $DB, $bill) = @_;
107 return _landing_fallback_plans() unless $bill->schema_ready($db, $dbh, $DB);
108
109 my @rows = $bill->list_plans($db, $dbh, $DB);
110 return _landing_fallback_plans() unless scalar @rows;
111
112 my $entitle_ready = $bill->plan_features_ready($db, $dbh, $DB);
113 my @catalog = $bill->feature_catalog;
114
115 my %by_tier;
116 foreach my $r (@rows) { push @{ $by_tier{ $r->{tier} } }, $r; }
117
118 my @tiers_seen;
119 foreach my $r (@rows) {
120 next if grep { $_ eq $r->{tier} } @tiers_seen;
121 push @tiers_seen, $r->{tier};
122 }
123
124 my @out;
125 foreach my $tier (@tiers_seen) {
126 my $monthly = (grep { ($_->{cadence}||'') eq 'monthly' } @{ $by_tier{$tier} })[0]
127 || $by_tier{$tier}->[0];
128 next unless $monthly;
129
130 # Build a short bullet list. Prefer the marketing copy supplied
131 # by the admin; otherwise synthesise from entitlements so the
132 # card never renders empty on a fresh install.
133 my @bullets = _parse_features_for_landing($monthly->{features});
134 if (!scalar @bullets && $entitle_ready) {
135 my $set = $bill->plan_feature_set($db, $dbh, $DB, $monthly->{id});
136 foreach my $f (@catalog) {
137 next unless $set->{ $f->{key} };
138 push @bullets, $f->{name} . ' &mdash; ' . $f->{blurb};
139 }
140 }
141 # Cap to 5 bullets to match the original card visual density.
142 @bullets = @bullets[0 .. ( $#bullets > 4 ? 4 : $#bullets )];
143 my $bullets_html = '';
144 foreach my $b (@bullets) {
145 $bullets_html .= '<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">'
146 . '<polyline points="20 6 9 17 4 12"/></svg> ' . $b . '</li>';
147 }
148
149 my $tier_slug = lc($monthly->{tier} || ''); $tier_slug =~ s/[^a-z0-9_\-]//g;
150
151 my $is_ent = $monthly->{is_enterprise} ? 1 : 0;
152 push @out, {
153 id => $monthly->{id},
154 tier => $monthly->{tier},
155 display_name => $monthly->{display_name} || ucfirst($monthly->{tier} || ''),
156 price_display => $is_ent ? 'Contact us' : _fmt_price_short($monthly->{price_cents}),
157 is_enterprise => $is_ent,
158 not_enterprise => $is_ent ? 0 : 1,
159 is_featured => $monthly->{is_featured} ? 1 : 0,
160 not_featured => $monthly->{is_featured} ? 0 : 1,
161 tagline => $monthly->{tagline} || '',
162 has_tagline => $monthly->{tagline} ? 1 : 0,
163 cta_label => $monthly->{cta_label}
164 || ($is_ent ? 'Contact sales' : 'Start 14-day free trial'),
165 cta_href => $is_ent
166 ? 'mailto:sales@3dshawn.com?subject=ContactForge%20Enterprise%20inquiry'
167 : ('signup.cgi?plan=' . $tier_slug),
168 bullets_html => $bullets_html,
169 };
170 }
171 return @out;
172}
173
174sub _fmt_price_short {
175 my ($cents) = @_;
176 $cents = 0 unless defined $cents;
177 if ($cents % 100 == 0) {
178 return '$' . int($cents / 100);
179 }
180 return '$' . sprintf('%.2f', $cents / 100);
181}
182
183sub _parse_features_for_landing {
184 my ($raw) = @_;
185 return () unless defined $raw && $raw =~ /\S/;
186 my @out;
187 while ($raw =~ m/\{\s*"name"\s*:\s*"((?:\\.|[^"\\])*)"\s*,\s*"included"\s*:\s*(true|false|0|1)\s*\}/gi) {
188 my $name = $1; my $inc = lc $2;
189 $name =~ s/\\"/"/g; $name =~ s/\\\\/\\/g;
190 # Landing block only shows the positive bullets.
191 push @out, $name if $inc eq 'true' || $inc eq '1';
192 }
193 return @out;
194}
195
196sub _landing_fallback_plans {
197 my $li = sub {
198 my @items = @_;
199 my $h = '';
200 foreach my $t (@items) {
201 $h .= '<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">'
202 . '<polyline points="20 6 9 17 4 12"/></svg> ' . $t . '</li>';
203 }
204 return $h;
205 };
206 # Post pricing-v4 (2026-06): Pro + Business + Enterprise.
207 return (
208 { tier=>'pro', display_name=>'Pro', price_display=>'$29',
209 is_enterprise=>0, not_enterprise=>1,
210 is_featured=>1, not_featured=>0,
211 tagline=>'Everything a growing sales team needs.',
212 has_tagline=>1, cta_label=>'Start 14-day free trial', cta_href=>'signup.cgi?plan=pro',
213 bullets_html => $li->('Up to 25,000 contacts', '5 team seats', 'Unlimited pipelines', '5,000 campaign sends / month', 'Connect your own SMTP') },
214 { tier=>'business', display_name=>'Business', price_display=>'$99',
215 is_enterprise=>0, not_enterprise=>1,
216 is_featured=>0, not_featured=>1,
217 tagline=>'For growing sales teams that need real scale.',
218 has_tagline=>1, cta_label=>'Start 14-day free trial', cta_href=>'signup.cgi?plan=business',
219 bullets_html => $li->('Up to 100,000 contacts', 'Up to 25 team seats', '50,000 campaign sends / month', 'API access + SSO + audit log export', 'Priority support') },
220 );
221}
222
223#======================================================================
224# Marketing page renderers (Group 6 merge)
225# Each corresponds to a former standalone .cgi (about/contact/faq/
226# features/help/privacy/terms/why). All use the same header +
227# template + wrapper pattern. help.cgi is the only one that requires
228# login.
229#======================================================================
230sub _mkt_render_about {
231 my ($userinfo) = @_;
232 print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
233 my $body = join('', $tfile->template('cf_about.html', {}, $userinfo));
234 $wrap->render({ userinfo => $userinfo, title => 'About', body => $body });
235 return;
236}
237
238sub _mkt_render_contact {
239 my ($userinfo) = @_;
240 print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
241 my $body = join('', $tfile->template('cf_contact.html', {}, $userinfo));
242 $wrap->render({ userinfo => $userinfo, title => 'Contact', body => $body });
243 return;
244}
245
246sub _mkt_render_faq {
247 my ($userinfo) = @_;
248 print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
249 my $body = join('', $tfile->template('cf_faq.html', {}, $userinfo));
250 $wrap->render({ userinfo => $userinfo, title => 'FAQ', body => $body });
251 return;
252}
253
254sub _mkt_render_features {
255 my ($userinfo) = @_;
256 print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
257 my $body = join('', $tfile->template('cf_features.html', {}, $userinfo));
258 $wrap->render({ userinfo => $userinfo, title => 'Features', body => $body });
259 return;
260}
261
262# help.cgi is login-gated (topbar help icon target); redirect anon
263# visitors to /login.cgi.
264sub _mkt_render_help {
265 my ($userinfo) = @_;
266 if (!$userinfo) {
267 print "Status: 302 Found\nLocation: /login.cgi\n\n";
268 return;
269 }
270 print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
271 my $body = join('', $tfile->template('cf_help.html', {}, $userinfo));
272 $wrap->render({
273 userinfo => $userinfo,
274 page_key => '',
275 title => 'Help',
276 body => $body,
277 });
278 return;
279}
280
281sub _mkt_render_privacy {
282 my ($userinfo) = @_;
283 print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
284 my $body = join('', $tfile->template('cf_privacy.html', {}, $userinfo));
285 $wrap->render({ userinfo => $userinfo, title => 'Privacy Policy', body => $body });
286 return;
287}
288
289sub _mkt_render_terms {
290 my ($userinfo) = @_;
291 print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
292 my $body = join('', $tfile->template('cf_terms.html', {}, $userinfo));
293 $wrap->render({ userinfo => $userinfo, title => 'Terms of Service', body => $body });
294 return;
295}
296
297sub _mkt_render_why {
298 my ($userinfo) = @_;
299 print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
300 my $body = join('', $tfile->template('cf_why.html', {}, $userinfo));
301 $wrap->render({ userinfo => $userinfo, title => 'Why ContactForge', body => $body });
302 return;
303}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help