Diff -- /var/www/vhosts/3dshawn.com/repricer.3dshawn.com/MODS/RePricer/Config.pm
Diff

/var/www/vhosts/3dshawn.com/repricer.3dshawn.com/MODS/RePricer/Config.pm

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

Added
+458
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to b4416083eeee
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1package MODS::RePricer::Config;
2#======================================================================
3# RePricer — central configuration.
4#
5# Lookup order for settings():
6# 1. platform_settings table (DB-backed) -- editable via
7# /admin_config.cgi by a super-admin. Highest precedence so an
8# admin can override anything without a code deploy.
9# 2. /var/www/vhosts/repricer.com/private/stripe.conf -- legacy
10# bootstrap path for the Stripe secret + webhook secret on a
11# brand-new install (before the admin has opened the config page).
12# 3. The hardcoded %DEFAULTS hash below -- final fallback. These are
13# also the deploy-coupled keys that must stay in code so they're
14# readable before the DB is reachable (database_name, asset paths,
15# cookie/session, feature-flag fallbacks).
16#
17# Lookups are cached per request inside %_DB_CACHE so a page that
18# reads ten settings only hits the DB once. The DB probe is column-
19# existence-guarded so pre-migration installs degrade cleanly to the
20# file + code fallbacks.
21#======================================================================
22
23use strict;
24use warnings;
25
26# Whitelist of keys the admin can override via the Software
27# Configuration page. Anything not in this list is read straight from
28# %DEFAULTS and is_secret marking controls UI masking.
29#
30# Note: this also defines what /admin_config.cgi can WRITE. Adding a
31# key here is the one-line "expose this to the admin" switch.
32my %EDITABLE = (
33 session_minutes => {
34 label => 'Session length (minutes)',
35 group => 'Defaults', secret => 0,
36 description => 'How long a user stays signed in after login. Default 720 = 12 hours. Affects both server-side user_sessions.expires_at AND the browser cookie Max-Age. Per-account override at users.session_minutes_override takes precedence.',
37 },
38 # Stripe
39 stripe_publishable => {
40 label => 'Stripe publishable key',
41 group => 'Stripe', secret => 0,
42 description => 'From Stripe Dashboard > Developers > API keys. Starts with pk_live_ for production or pk_test_ for test mode. Safe to expose to browsers -- it is embedded in the card-entry form on /billing_payment.cgi so Stripe Elements can tokenize card numbers without the PAN ever touching our server.',
43 },
44 stripe_secret => {
45 label => 'Stripe secret key',
46 group => 'Stripe', secret => 1,
47 description => 'From Stripe Dashboard > Developers > API keys. Starts with sk_live_ or sk_test_. NEVER paste this anywhere outside this field -- anyone with this key can charge cards on your Stripe account. Server-side code uses it to create customers, SetupIntents, and PaymentIntents.',
48 },
49 # stripe_connect_client removed 2026-06-21 -- RePricer doesn't host
50 # buyer-to-seller transactions (sellers' marketplaces do that), so
51 # Stripe Connect isn't part of the platform.
52 stripe_webhook_secret => {
53 label => 'Stripe webhook signing secret',
54 group => 'Stripe', secret => 1,
55 description => 'From Stripe Dashboard > Developers > Webhooks > your endpoint > "Signing secret". Starts with whsec_. Used to verify incoming webhook calls really came from Stripe. Set this AFTER pointing the webhook at https://repricer.3dshawn.com/stripe_webhook.cgi -- Stripe generates the secret when you create the endpoint.',
56 },
57 # stripe_platform_fee_bps removed 2026-06-21 -- there are no
58 # buyer-to-seller destination charges on RePricer.
59 stripe_currency_default => {
60 label => 'Default currency code',
61 group => 'Stripe', secret => 0,
62 description => 'Three-letter ISO 4217 code, lowercase: usd, eur, gbp, cad. Used when a transaction does not specify its own currency. Only affects Stripe API calls (RePricer subscription billing).',
63 },
64
65 # Branding
66 website_title => {
67 label => 'Site title',
68 group => 'Branding', secret => 0,
69 description => 'The HTML <title> tag value -- shown in browser tabs, search results, and link previews. HTML entities are allowed (e.g. —).',
70 },
71 brand_name => {
72 label => 'Brand name',
73 group => 'Branding', secret => 0,
74 description => 'Short product name shown in the wrapper header, profile dropdown, marketing pages, and the From-line on system emails.',
75 },
76 brand_tagline => {
77 label => 'Tagline',
78 group => 'Branding', secret => 0,
79 description => 'One-line tagline shown under the brand name in the sidebar header and a few marketing pages.',
80 },
81
82 # Email
83 feedback_emails => {
84 label => 'Support / feedback inbox',
85 group => 'Email', secret => 0,
86 description => 'Inbox that receives "Contact us" form submissions. Comma-separate for multiple recipients (e.g. "support@example.com, ops@example.com").',
87 },
88 from_email => {
89 label => 'Outbound "From" address',
90 group => 'Email', secret => 0,
91 description => 'Sender address on system-generated emails (password resets, receipts, invoices). Must be on a domain whose SPF and DKIM records you control -- otherwise messages land in spam.',
92 },
93 smtp_host => {
94 label => 'SMTP host',
95 group => 'Email', secret => 0,
96 description => 'SMTP server hostname for outbound mail (e.g. smtp.gmail.com, smtp.sendgrid.net, smtp.postmarkapp.com). Leave blank to use the server\'s local sendmail / Plesk MTA.',
97 },
98 smtp_port => {
99 label => 'SMTP port',
100 group => 'Email', secret => 0,
101 description => 'Usually 587 for STARTTLS or 465 for implicit TLS. Only consulted when SMTP host is set.',
102 },
103 smtp_user => {
104 label => 'SMTP username',
105 group => 'Email', secret => 0,
106 description => 'Authentication username for the SMTP server. Often your full email address. Leave blank if your SMTP server allows unauthenticated relay (uncommon).',
107 },
108 smtp_pass => {
109 label => 'SMTP password',
110 group => 'Email', secret => 1,
111 description => 'Authentication password (or app-specific password / API token) for the SMTP server. Stored encrypted at rest; never echoed back to the browser after save.',
112 },
113
114 # File storage (R2) removed 2026-06-21 -- R2 was used for buyer-
115 # downloaded 3D model files on the storefront. RePricer doesn't host
116 # uploaded files (sellers' marketplaces own their listing images).
117
118 # Defaults
119 timezone_default => {
120 label => 'Default timezone',
121 group => 'Defaults', secret => 0,
122 description => 'IANA timezone name -- UTC, America/New_York, Europe/Berlin, Asia/Tokyo. New users inherit this until they pick their own in Preferences. Affects display only; the database stores timestamps in server time.',
123 },
124 currency_default => {
125 label => 'Default currency',
126 group => 'Defaults', secret => 0,
127 description => 'Three-letter ISO 4217 code (USD, EUR, GBP, CAD). New users inherit this for their storefront display currency. Affects display only -- order amounts are stored in the currency they were transacted in.',
128 },
129
130 # ----- Amazon SP-API -----------------------------------------
131 # App-level credentials -- one set for the whole RePricer install.
132 # Sellers connect their own accounts via OAuth from marketplaces.cgi;
133 # the LWA refresh token RePricer receives is stored on each seller's
134 # marketplace_accounts row, not here.
135 amazon_lwa_client_id => {
136 label => 'Amazon LWA client ID',
137 group => 'Amazon', secret => 0,
138 description => 'From Seller Central > Apps & Services > Develop Apps > your app > "LWA credentials" > Client identifier. Identifies the RePricer SaaS to Amazon during OAuth.',
139 },
140 amazon_lwa_client_secret => {
141 label => 'Amazon LWA client secret',
142 group => 'Amazon', secret => 1,
143 description => 'From the same screen as the LWA client ID. Used by oauth_amazon.cgi to exchange the authorization code for a refresh token. NEVER expose to a browser -- the OAuth code-for-token swap is strictly server-side.',
144 },
145 amazon_sp_app_id => {
146 label => 'Amazon SP-API application ID',
147 group => 'Amazon', secret => 0,
148 description => 'From the SP-API developer console > your app > App ID (starts with "amzn1.sp.solution."). Used in the seller-consent authorize URL.',
149 },
150 amazon_oauth_redirect => {
151 label => 'Amazon OAuth redirect URI',
152 group => 'Amazon', secret => 0,
153 description => 'The full URL Amazon should redirect to after the seller grants access -- typically https://repricer.3dshawn.com/oauth_amazon.cgi. Must match EXACTLY what is registered in the SP-API developer console.',
154 },
155
156 # ----- eBay --------------------------------------------------
157 ebay_client_id => {
158 label => 'eBay App ID (client ID)',
159 group => 'eBay', secret => 0,
160 description => 'From developer.ebay.com > My Account > Application Keys > Production > App ID. Identifies the RePricer SaaS to eBay during OAuth.',
161 },
162 ebay_client_secret => {
163 label => 'eBay Cert ID (client secret)',
164 group => 'eBay', secret => 1,
165 description => 'From the same Application Keys screen -- "Cert ID" is eBay name for the OAuth client secret. Used by oauth_ebay.cgi to exchange the authorization code for tokens.',
166 },
167 ebay_runame => {
168 label => 'eBay RuName (redirect URL ID)',
169 group => 'eBay', secret => 0,
170 description => 'From developer.ebay.com > My Account > Application Keys > "User Tokens" > the RuName eBay generates for your redirect URL. eBay uses the RuName instead of the literal redirect URL in the authorize call.',
171 },
172 ebay_oauth_redirect => {
173 label => 'eBay OAuth redirect URL',
174 group => 'eBay', secret => 0,
175 description => 'Full https URL eBay should redirect to after the seller grants access -- typically https://repricer.3dshawn.com/oauth_ebay.cgi. Must be registered under the RuName in the developer console.',
176 },
177 ebay_env => {
178 label => 'eBay environment',
179 group => 'eBay', secret => 0,
180 description => 'Either "production" or "sandbox". Use "sandbox" while developing or before your eBay app has graduated to production. Both modes need separate App ID + Cert ID + RuName from the developer portal.',
181 },
182
183 # ----- Walmart -----------------------------------------------
184 # Walmart uses per-seller Consumer ID + Private Key (RSA) rather
185 # than OAuth. There are NO app-level credentials -- the seller
186 # pastes their own pair into the Connect Walmart form on
187 # marketplaces.cgi, and we sign every request with it.
188);
189
190my %DEFAULTS = (
191 # ----- Database ----------------------------------------------
192 # NOT editable via UI -- the lookup itself depends on this.
193 database_name => 'repricer3dshawn',
194
195 # ----- Branding ----------------------------------------------
196 website_title => 'Repricer — Automated Amazon & Walmart Repricing',
197 brand_name => 'Repricer',
198 brand_tagline => 'Win the buy box, defend your margin, sleep at night.',
199
200 # ----- Sessions / cookies ------------------------------------
201 # NOT editable via UI -- deploy-coupled. Edit MODS/RePricer/Config.pm.
202 cookie_name => 'repricer_session',
203 session_minutes => 720,
204 secure_cookies => 1,
205 # Host-only cookie -- session lives on repricer.3dshawn.com only.
206 cookie_domain => '',
207
208 # ----- Asset paths -------------------------------------------
209 # NOT editable via UI -- deploy-coupled.
210 assets => '/assets',
211 assets_css => '/assets/css',
212 assets_js => '/assets/javascript',
213 assets_images => '/assets/images',
214
215 # ----- Email / mail ------------------------------------------
216 feedback_emails => 'programmershawn@gmail.com',
217 from_email => 'noreply@repricer.3dshawn.com',
218 smtp_host => '', # blank = use local sendmail
219 smtp_port => 587,
220 smtp_user => '',
221 smtp_pass => '',
222
223 # ----- Stripe ------------------------------------------------
224 stripe_publishable => 'pk_test_REPLACE_ME',
225 stripe_secret => '',
226 stripe_webhook_secret => '',
227 stripe_currency_default => 'usd',
228
229 # ----- Marketplace API app credentials -----------------------
230 # All blank by default. Pasted in via /admin_config.cgi (Amazon /
231 # eBay groups). See MODS/RePricer/Marketplaces/*.pm adapters.
232 amazon_lwa_client_id => '',
233 amazon_lwa_client_secret => '',
234 amazon_sp_app_id => '',
235 amazon_oauth_redirect => 'https://repricer.3dshawn.com/oauth_amazon.cgi',
236 ebay_client_id => '',
237 ebay_client_secret => '',
238 ebay_runame => '',
239 ebay_oauth_redirect => 'https://repricer.3dshawn.com/oauth_ebay.cgi',
240 ebay_env => 'production',
241
242 # (File storage / R2 defaults removed 2026-06-21 -- RePricer doesn't
243 # host uploaded files.)
244
245 # ----- Plan limits (pricing v3) ------------------------------
246 # Trial SKU cap applies for the 14-day window regardless of tier.
247 plan_trial_sku_cap => 10,
248 plan_pro_price => 49,
249 plan_business_price => 149,
250 plan_scale_price => 349,
251
252 # ----- Feature flags fallback (when DB unreachable) ----------
253 # NOT editable via UI -- read when DB is DOWN, so DB rows would
254 # never apply anyway. Keep in code.
255 flag_default__module_publishing => 1,
256 flag_default__module_pooled_data => 0,
257 # (module_storefront / module_optimization / module_audience flags
258 # removed 2026-06-21 with the storefront infrastructure.)
259
260 # ----- Misc --------------------------------------------------
261 timezone_default => 'UTC',
262 currency_default => 'USD',
263);
264
265# Per-process cache. Populated on first DB hit; survives for the
266# request and is cheap on subsequent settings() calls.
267my %_DB_CACHE;
268my $_DB_LOADED = 0;
269my $_DB_AVAILABLE; # undef = unknown, 0 = table missing, 1 = ready
270
271sub new {
272 my ($class) = @_;
273 return bless({}, $class);
274}
275
276# Public helper for the admin config page. Returns the editable
277# whitelist with current values + source ('db' | 'file' | 'default').
278# is_secret rows have their value masked to a non-empty marker so the
279# UI can show "set" without leaking the actual secret.
280sub editable_settings_meta {
281 my ($self) = @_;
282 my @rows;
283 foreach my $key (sort keys %EDITABLE) {
284 my $meta = $EDITABLE{$key};
285 my ($val, $source) = $self->_resolve_with_source($key);
286 my $masked = ($meta->{secret} && defined($val) && length $val) ? 1 : 0;
287 push @rows, {
288 key => $key,
289 label => $meta->{label},
290 group => $meta->{group},
291 secret => $meta->{secret},
292 description => $meta->{description} || '',
293 value => $masked ? '' : (defined $val ? $val : ''),
294 is_set => (defined $val && length $val) ? 1 : 0,
295 source => $source,
296 };
297 }
298 return @rows;
299}
300
301# Bool -- is this key editable via the admin config page?
302sub is_editable {
303 my ($self, $key) = @_;
304 return exists $EDITABLE{$key} ? 1 : 0;
305}
306
307sub editable_meta_for {
308 my ($self, $key) = @_;
309 return $EDITABLE{$key};
310}
311
312sub settings {
313 my ($self, $key) = @_;
314 return undef unless defined $key;
315
316 my ($val) = $self->_resolve_with_source($key);
317 return $val;
318}
319
320# Core lookup. Returns ($value, $source) where source is one of
321# 'db' | 'file' | 'default'. Centralises the precedence so any caller
322# (settings() / editable_settings_meta) gets the same answer.
323sub _resolve_with_source {
324 my ($self, $key) = @_;
325
326 # 1) DB lookup (cached). Skip for the database_name itself -- we
327 # can't ask the DB which DB to use.
328 if ($key ne 'database_name') {
329 $self->_load_db_cache;
330 if (exists $_DB_CACHE{$key}) {
331 return ($_DB_CACHE{$key}, 'db');
332 }
333 }
334
335 # 2) Private file fallback (legacy Stripe secret path). Keeps
336 # brand-new installs working until the admin opens the config
337 # page.
338 if ($key eq 'stripe_secret' || $key eq 'stripe_webhook_secret') {
339 my $val = _read_private('stripe.conf', $key);
340 return ($val, 'file') if defined $val && length $val;
341 }
342
343 # 3) Hardcoded default.
344 return (exists $DEFAULTS{$key} ? $DEFAULTS{$key} : undef, 'default');
345}
346
347# Single-pass population of %_DB_CACHE. Probes for the table to avoid
348# crashing pre-migration installs. Silently bails on any error so
349# Config.pm never takes the site down.
350sub _load_db_cache {
351 my ($self) = @_;
352 return if $_DB_LOADED;
353 $_DB_LOADED = 1;
354
355 # We deliberately defer the require until called rather than at
356 # module-load -- avoids a circular use between Config and DBConnect
357 # in early-boot paths.
358 my $db_pkg = 'MODS::DBConnect';
359 eval "require $db_pkg; 1" or return;
360 my $db = $db_pkg->new;
361 return unless $db;
362
363 my $dbh = eval { $db->db_connect() };
364 return unless $dbh;
365
366 my $DB = $DEFAULTS{database_name};
367
368 my $col_check = eval {
369 $db->db_readwrite($dbh, qq~
370 SELECT COUNT(*) AS n FROM information_schema.tables
371 WHERE table_schema='$DB' AND table_name='platform_settings'
372 ~, $ENV{SCRIPT_NAME}, __LINE__);
373 };
374 unless ($col_check && $col_check->{n}) {
375 $_DB_AVAILABLE = 0;
376 eval { $db->db_disconnect($dbh) };
377 return;
378 }
379 $_DB_AVAILABLE = 1;
380
381 my @rows;
382 eval {
383 @rows = $db->db_readwrite_multiple($dbh, qq~
384 SELECT setting_key, setting_value
385 FROM `${DB}`.platform_settings
386 ~, $ENV{SCRIPT_NAME}, __LINE__);
387 1;
388 };
389 foreach my $r (@rows) {
390 next unless ref($r) eq 'HASH';
391 next unless defined $r->{setting_key};
392 my $val = defined $r->{setting_value} ? $r->{setting_value} : '';
393 next unless length $val; # empty rows defer to file/code so admins can "blank" a field
394 $_DB_CACHE{ $r->{setting_key} } = $val;
395 }
396 eval { $db->db_disconnect($dbh) };
397}
398
399# Reads /var/www/vhosts/repricer.com/private/<file> and returns the
400# value for the requested key. Returns undef if the file or key is
401# missing. Cached per-process.
402my %_PRIVATE_CACHE;
403sub _read_private {
404 my ($file, $key) = @_;
405 my $path = '/var/www/vhosts/repricer.com/private/' . $file;
406 unless (exists $_PRIVATE_CACHE{$path}) {
407 my %h;
408 if (open(my $fh, '<', $path)) {
409 while (my $line = <$fh>) {
410 chomp $line;
411 next if $line =~ /^\s*#/;
412 next unless $line =~ /^([A-Za-z0-9_]+)\s*=\s*(.*)$/;
413 $h{$1} = $2;
414 }
415 close $fh;
416 }
417 $_PRIVATE_CACHE{$path} = \%h;
418 }
419 return $_PRIVATE_CACHE{$path}->{$key};
420}
421
422# Returns the {url_key => "/path?cachekey"} hash used by Template.pm's
423# [url:foo] tag. Mirrors MODS::Urls but kept RePricer-local so we never
424# collide with the existing portal URL registry.
425sub urls {
426 my $self = shift;
427 return {
428 home => '/index.cgi',
429 login => '/login.cgi',
430 signup => '/signup.cgi',
431 logout => '/logout.cgi',
432 dashboard => '/dashboard.cgi',
433 # Repricer-native
434 products => '/products.cgi',
435 competitors => '/competitors.cgi',
436 rules => '/repricing_rules.cgi',
437 price_history => '/price_history.cgi',
438 reports => '/sales_reports.cgi',
439 marketplaces => '/marketplaces.cgi',
440 billing => '/billing.cgi',
441 # Legacy aliases kept so old templates keep resolving until pruned.
442 models => '/products.cgi',
443 upload => '/products.cgi',
444 storefront => '/products.cgi',
445 analytics => '/sales_reports.cgi',
446 optimization => '/repricing_rules.cgi',
447 audience => '/competitors.cgi',
448 profile => '/profile.cgi',
449 preferences => '/preferences.cgi',
450 admin => '/admin.cgi',
451 assets => '/assets',
452 assets_css => '/assets/css',
453 assets_js => '/assets/javascript',
454 assets_images => '/assets/images',
455 };
456}
457
4581;
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help