Diff -- /var/www/vhosts/3dshawn.com/repricer.3dshawn.com/pricing.cgi
Diff

/var/www/vhosts/3dshawn.com/repricer.3dshawn.com/pricing.cgi

added on local at 2026-07-01 21:47:16

Added
+266
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to d3682efca5ac
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# RePricer - Pricing page (logged-out marketing)
4#
5# Pulls live plan rows from billing_plans so the same data drives this
6# page, the landing-page pricing block, and the per-feature gate in
7# settings.cgi. Falls back to a built-in 4-plan list if the billing
8# schema hasn't been migrated yet so the marketing page can still
9# render on a fresh install.
10#======================================================================
11use strict;
12use warnings;
13
14use lib '/var/www/vhosts/3dshawn.com/repricer.3dshawn.com';
15use CGI;
16use MODS::Template;
17use MODS::DBConnect;
18use MODS::Login;
19use MODS::RePricer::Config;
20use MODS::RePricer::Wrapper;
21use MODS::RePricer::Billing;
22use MODS::RePricer::Promotions;
23
24my $q = CGI->new;
25my $auth = MODS::Login->new;
26my $wrap = MODS::RePricer::Wrapper->new;
27my $tfile = MODS::Template->new;
28my $db = MODS::DBConnect->new;
29my $cfg = MODS::RePricer::Config->new;
30my $bill = MODS::RePricer::Billing->new;
31my $DB = $cfg->settings('database_name');
32
33$|=1;
34my $userinfo = $auth->login_verify();
35
36my $dbh = $db->db_connect();
37my @plans = _build_plan_cards($db, $dbh, $DB, $bill);
38
39# Active plan-target promotion (if any) flows into every pricing card
40# below + into the hero banner so visitors see the deal before they
41# pick a tier. Falls back to "no promo" when there isn't an active row.
42my $promo_obj = MODS::RePricer::Promotions->new;
43my $active_promo = $promo_obj->active_plan_promo($db, $dbh, $DB);
44my $promo_code = ($active_promo && $active_promo->{code}) ? $active_promo->{code} : '';
45my $promo_label = $active_promo ? $promo_obj->marketing_label($active_promo) : '';
46my $promo_name = ($active_promo && $active_promo->{name}) ? $active_promo->{name} : '';
47
48# Per-card "with code WELCOME20 get 20% off your first invoice" line --
49# pre-baked here so the template just emits a single field per card.
50# Only the plans that match the promo's eligibility set get the badge;
51# an empty eligibility set means "every paid plan", so all paid cards
52# light up. Free is always excluded since there's nothing to discount.
53if ($active_promo) {
54 my @eligible = $promo_obj->eligible_plan_ids($db, $dbh, $DB, $active_promo->{id});
55 my %eligible_set = map { $_ => 1 } @eligible;
56 my $all_paid = !scalar(@eligible);
57 foreach my $c (@plans) {
58 next if $c->{is_free};
59 next unless $all_paid || $eligible_set{ $c->{id} || 0 };
60 $c->{promo_active} = 1;
61 $c->{promo_code} = $promo_code;
62 $c->{promo_label} = $promo_label;
63 $c->{promo_card_line} = $promo_label . ' on your first invoice with code <strong>' . $promo_code . '</strong>';
64 }
65}
66
67$db->db_disconnect($dbh);
68
69print "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";
70
71my $tvars = {
72 plans => \@plans,
73 has_plans => scalar(@plans) ? 1 : 0,
74 has_promo => $active_promo ? 1 : 0,
75 promo_code => $promo_code,
76 promo_label => $promo_label,
77 promo_name => $promo_name,
78};
79my $body = join('', $tfile->template('repricer_pricing.html', $tvars, $userinfo));
80$wrap->render({ userinfo => $userinfo, title => 'Pricing', body => $body });
81
82#----------------------------------------------------------------------
83# Each card needs: tier, display_name, price_display, period_label,
84# tagline, cta_label, cta_href, is_featured, and a list of feature
85# bullets (name + included flag).
86sub _build_plan_cards {
87 my ($db, $dbh, $DB, $bill) = @_;
88
89 return _fallback_plans() unless $bill->schema_ready($db, $dbh, $DB);
90
91 my $entitle_ready = $bill->plan_features_ready($db, $dbh, $DB);
92 my @catalog = $bill->feature_catalog;
93 my %catalog_by_key = map { $_->{key} => $_ } @catalog;
94
95 # Post-consolidation: one row per tier. Monthly price is the
96 # headline; yearly comes from the same row via the yearly_* fields
97 # (compute_yearly_price_cents resolves percent/dollars/explicit).
98 my @rows = $bill->list_plans($db, $dbh, $DB);
99 return _fallback_plans() unless scalar @rows;
100
101 my @out;
102 foreach my $r (@rows) {
103 my @bullets = _parse_marketing_features($r->{features});
104 if (!scalar @bullets && $entitle_ready) {
105 my $set = $bill->plan_feature_set($db, $dbh, $DB, $r->{id});
106 foreach my $f (@catalog) {
107 push @bullets, {
108 name => $f->{name} . ' &mdash; ' . $f->{blurb},
109 included => $set->{ $f->{key} } ? 1 : 0,
110 };
111 }
112 }
113
114 my $yearly_cents = $bill->compute_yearly_price_cents($r);
115 my $annual_note = '';
116 my $has_annual_note = 0;
117 if ($yearly_cents > 0) {
118 # Compute the buyer-facing "savings" tail. Months-free is
119 # only meaningful when the yearly price is below 12 months
120 # of the monthly price.
121 my $monthly_12 = int($r->{price_cents} || 0) * 12;
122 my $savings = $monthly_12 - $yearly_cents;
123 my $months_free = $monthly_12 > 0 ? int($savings * 12 / $monthly_12) : 0;
124 my $tail = '';
125 if ($months_free >= 2) {
126 $tail = " ($months_free months free)";
127 } elsif ($savings > 0) {
128 $tail = ' (save ' . _fmt_price_card($savings, $r->{currency}) . ')';
129 }
130 $annual_note = 'or ' . _fmt_price_card($yearly_cents, $r->{currency}) . ' / yr' . $tail;
131 $has_annual_note = 1;
132 } elsif (!$r->{price_cents}) {
133 $annual_note = 'forever &middot; no card required';
134 $has_annual_note = 1;
135 }
136
137 push @out, {
138 id => $r->{id},
139 tier => $r->{tier},
140 display_name => $r->{display_name} || ucfirst($r->{tier} || ''),
141 tagline => $r->{tagline} || '',
142 has_tagline => $r->{tagline} ? 1 : 0,
143 price_display => _fmt_price_card($r->{price_cents}, $r->{currency}),
144 is_free => ($r->{price_cents} || 0) == 0 ? 1 : 0,
145 period_label => 'mo',
146 annual_note => $annual_note,
147 has_annual_note => $has_annual_note,
148 cta_label => $r->{cta_label} || ($r->{price_cents} ? 'Start 14-day free trial' : 'Start free'),
149 cta_href => $r->{price_cents}
150 ? ('signup.cgi?plan=' . _safe_slug($r->{tier}))
151 : 'signup.cgi',
152 is_featured => $r->{is_featured} ? 1 : 0,
153 not_featured => $r->{is_featured} ? 0 : 1,
154 bullets_html => _render_bullets(\@bullets),
155 };
156 }
157 return @out;
158}
159
160# Pre-render bullet <li> HTML in Perl so the template can emit it as
161# a single variable -- side-steps the engine's nested-loop quirks.
162sub _render_bullets {
163 my ($aref) = @_;
164 my $html = '';
165 foreach my $b (@{ $aref || [] }) {
166 my $name = defined $b->{name} ? $b->{name} : '';
167 if ($b->{included}) {
168 $html .= '<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">'
169 . '<polyline points="20 6 9 17 4 12"/></svg> ' . $name . '</li>';
170 } else {
171 $html .= '<li class="dim"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">'
172 . '<line x1="6" y1="6" x2="18" y2="18"/><line x1="6" y1="18" x2="18" y2="6"/></svg> '
173 . $name . '</li>';
174 }
175 }
176 return $html;
177}
178
179sub _parse_marketing_features {
180 my ($raw) = @_;
181 return () unless defined $raw && $raw =~ /\S/;
182 # Minimal JSON: [{"name":"X","included":true},...]. We control the
183 # writes, so a tolerant regex parse is enough -- no JSON dep needed.
184 my @out;
185 while ($raw =~ m/\{\s*"name"\s*:\s*"((?:\\.|[^"\\])*)"\s*,\s*"included"\s*:\s*(true|false|0|1)\s*\}/gi) {
186 my $name = $1; my $inc = lc $2;
187 $name =~ s/\\"/"/g; $name =~ s/\\\\/\\/g;
188 push @out, {
189 name => $name,
190 included => ($inc eq 'true' || $inc eq '1') ? 1 : 0,
191 };
192 }
193 return @out;
194}
195
196sub _fmt_price_card {
197 my ($cents, $currency) = @_;
198 $cents = 0 unless defined $cents;
199 # Plain '$'; the value is interpolated as data at eval time, NOT
200 # re-parsed as template syntax, so no \-escape needed.
201 if ($cents % 100 == 0) {
202 return '$' . int($cents / 100);
203 }
204 return '$' . sprintf('%.2f', $cents / 100);
205}
206
207sub _safe_slug {
208 my $s = lc(shift || '');
209 $s =~ s/[^a-z0-9_\-]//g;
210 return $s;
211}
212
213# Used when billing tables haven't been migrated yet. Keeps the
214# marketing page rendering for fresh installs. Matches the live
215# billing_plans rows (Pro / Business / Scale) post pricing-v3.
216sub _fallback_plans {
217 return (
218 { tier=>'pro', display_name=>'Pro', tagline=>'Defend Buy Box on Amazon.', has_tagline=>1,
219 price_display=>'$49', is_free=>0, period_label=>'mo',
220 annual_note=>'or $490 / yr (2 months free)', has_annual_note=>1,
221 cta_label=>'Start 14-day free trial', cta_href=>'signup.cgi?plan=pro',
222 is_featured=>1, not_featured=>0,
223 bullets_html=>_render_bullets([
224 { name=>'Up to 1,000 SKUs under management', included=>1 },
225 { name=>'Reprice every 5 minutes', included=>1 },
226 { name=>'Amazon marketplace', included=>1 },
227 { name=>'All 8 built-in strategies', included=>1 },
228 { name=>'Hard floor, ceiling, and MAP guardrails', included=>1 },
229 { name=>'Full audit log', included=>1 },
230 { name=>'Email support', included=>1 },
231 { name=>'Walmart marketplace', included=>0 },
232 { name=>'eBay marketplace', included=>0 },
233 { name=>'Custom strategies', included=>0 },
234 ]) },
235 { tier=>'business', display_name=>'Business', tagline=>'Multi-marketplace, full automation.', has_tagline=>1,
236 price_display=>'$149', is_free=>0, period_label=>'mo',
237 annual_note=>'or $1,490 / yr (2 months free)', has_annual_note=>1,
238 cta_label=>'Start 14-day free trial', cta_href=>'signup.cgi?plan=business',
239 is_featured=>0, not_featured=>1,
240 bullets_html=>_render_bullets([
241 { name=>'<strong>Everything in Pro</strong>', included=>1 },
242 { name=>'Up to 10,000 SKUs under management', included=>1 },
243 { name=>'Reprice every minute', included=>1 },
244 { name=>'Amazon + Walmart marketplaces', included=>1 },
245 { name=>'Audit log CSV export', included=>1 },
246 { name=>'Priority support (same business day)', included=>1 },
247 { name=>'eBay marketplace', included=>0 },
248 { name=>'Custom strategies', included=>0 },
249 { name=>'SLA-backed support', included=>0 },
250 ]) },
251 { tier=>'scale', display_name=>'Scale', tagline=>'Enterprise scale, custom strategies, SLA.', has_tagline=>1,
252 price_display=>'$349', is_free=>0, period_label=>'mo',
253 annual_note=>'or $3,490 / yr (2 months free)', has_annual_note=>1,
254 cta_label=>'Start 14-day free trial', cta_href=>'signup.cgi?plan=scale',
255 is_featured=>0, not_featured=>1,
256 bullets_html=>_render_bullets([
257 { name=>'<strong>Everything in Business</strong>', included=>1 },
258 { name=>'Unlimited SKUs', included=>1 },
259 { name=>'Amazon + Walmart + eBay', included=>1 },
260 { name=>'Custom repricing strategies', included=>1 },
261 { name=>'99.95% uptime SLA with service credits', included=>1 },
262 { name=>'Dedicated customer success manager', included=>1 },
263 { name=>'SAML SSO + audit log API', included=>1 },
264 ]) },
265 );
266}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help