added on local at 2026-07-01 21:47:10
| 1 | package 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 | ||
| 23 | use strict; | |
| 24 | use 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. | |
| 32 | my %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 | ||
| 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 | ||
| 190 | my %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. | |
| 267 | my %_DB_CACHE; | |
| 268 | my $_DB_LOADED = 0; | |
| 269 | my $_DB_AVAILABLE; # undef = unknown, 0 = table missing, 1 = ready | |
| 270 | ||
| 271 | sub 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. | |
| 280 | sub 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? | |
| 302 | sub is_editable { | |
| 303 | my ($self, $key) = @_; | |
| 304 | return exists $EDITABLE{$key} ? 1 : 0; | |
| 305 | } | |
| 306 | ||
| 307 | sub editable_meta_for { | |
| 308 | my ($self, $key) = @_; | |
| 309 | return $EDITABLE{$key}; | |
| 310 | } | |
| 311 | ||
| 312 | sub 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. | |
| 323 | sub _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. | |
| 350 | sub _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. | |
| 402 | my %_PRIVATE_CACHE; | |
| 403 | sub _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. | |
| 425 | sub 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 | ||
| 458 | 1; |