added on local at 2026-07-01 21:47:12
| 1 | package MODS::RePricer::Marketplaces; | |
| 2 | ||
| 3 | sub new { | |
| 4 | my ($cls) = @_; | |
| 5 | return bless {}, ref($cls) || $cls; | |
| 6 | } | |
| 7 | ||
| 8 | #====================================================================== | |
| 9 | # RePricer -- marketplace registry. | |
| 10 | # | |
| 11 | # Single source of truth for which marketplaces RePricer supports as | |
| 12 | # repricing targets. Each entry describes the marketplace, how a seller | |
| 13 | # connects, and which adapter module implements the API plumbing. | |
| 14 | # | |
| 15 | # To add a new marketplace: | |
| 16 | # 1. Append an entry to @PLATFORMS. | |
| 17 | # 2. Drop a module at MODS/RePricer/Marketplaces/<Slug>.pm whose | |
| 18 | # package is MODS::RePricer::Marketplaces::<Slug> and which | |
| 19 | # implements the adapter contract: | |
| 20 | # new($self, $account) # $account = marketplace_accounts row | |
| 21 | # test_connection() # cheap call; { ok => 1 } or { error => '...' } | |
| 22 | # list_my_listings($opts) # paged seller's own listings | |
| 23 | # get_competitor_offers($product) # competitor pricing for one product | |
| 24 | # update_price($product, $cents) # push new price; { ok => 1, id => '...' } | |
| 25 | # refresh_token_if_needed() # for OAuth-based platforms | |
| 26 | # 3. marketplaces.cgi auto-picks up the new entry for the Connect UI. | |
| 27 | #====================================================================== | |
| 28 | use strict; | |
| 29 | use warnings; | |
| 30 | ||
| 31 | our @PLATFORMS = ( | |
| 32 | { | |
| 33 | slug => 'amazon', | |
| 34 | name => 'Amazon', | |
| 35 | tagline => 'The big one. Pricing + Listings APIs via SP-API; LWA OAuth.', | |
| 36 | adapter => 'MODS::RePricer::Marketplaces::Amazon', | |
| 37 | auth => 'oauth', | |
| 38 | regions => [ | |
| 39 | { code => 'us', label => 'United States', marketplace_id => 'ATVPDKIKX0DER' }, | |
| 40 | { code => 'ca', label => 'Canada', marketplace_id => 'A2EUQ1WTGCTBG2' }, | |
| 41 | { code => 'mx', label => 'Mexico', marketplace_id => 'A1AM78C64UM0Y8' }, | |
| 42 | { code => 'uk', label => 'United Kingdom', marketplace_id => 'A1F83G8C2ARO7P' }, | |
| 43 | { code => 'de', label => 'Germany', marketplace_id => 'A1PA6795UKMFR9' }, | |
| 44 | { code => 'fr', label => 'France', marketplace_id => 'A13V1IB3VIYZZH' }, | |
| 45 | { code => 'it', label => 'Italy', marketplace_id => 'APJ6JRA9NG5V4' }, | |
| 46 | { code => 'es', label => 'Spain', marketplace_id => 'A1RKKUPIHCS9HS' }, | |
| 47 | { code => 'jp', label => 'Japan', marketplace_id => 'A1VC38T7YXB528' }, | |
| 48 | { code => 'au', label => 'Australia', marketplace_id => 'A39IBJ37TRP1C6' }, | |
| 49 | ], | |
| 50 | signup_url => 'https://sell.amazon.com/', | |
| 51 | developer_url=> 'https://developer-docs.amazon.com/sp-api/', | |
| 52 | seller_plan_required => 'Professional ($39.99/mo)', | |
| 53 | oauth_settings => { | |
| 54 | client_id_key => 'amazon_lwa_client_id', | |
| 55 | client_secret_key => 'amazon_lwa_client_secret', | |
| 56 | app_id_key => 'amazon_sp_app_id', | |
| 57 | refresh_url => 'https://api.amazon.com/auth/o2/token', | |
| 58 | authorize_url => 'https://sellercentral.amazon.com/apps/authorize/consent', | |
| 59 | callback_cgi => '/oauth_amazon.cgi', | |
| 60 | scope => '', # SP-API doesn't use OAuth scopes; permissions come from app roles | |
| 61 | }, | |
| 62 | icon => 'A', | |
| 63 | accent => '#ff9900', | |
| 64 | }, | |
| 65 | { | |
| 66 | slug => 'ebay', | |
| 67 | name => 'eBay', | |
| 68 | tagline => 'Fixed-price Buy It Now listings. OAuth2; Sell + Browse APIs.', | |
| 69 | adapter => 'MODS::RePricer::Marketplaces::EBay', | |
| 70 | auth => 'oauth', | |
| 71 | regions => [ | |
| 72 | { code => 'us', label => 'United States', marketplace_id => 'EBAY_US' }, | |
| 73 | { code => 'uk', label => 'United Kingdom', marketplace_id => 'EBAY_GB' }, | |
| 74 | { code => 'de', label => 'Germany', marketplace_id => 'EBAY_DE' }, | |
| 75 | { code => 'au', label => 'Australia', marketplace_id => 'EBAY_AU' }, | |
| 76 | { code => 'ca', label => 'Canada', marketplace_id => 'EBAY_CA' }, | |
| 77 | { code => 'fr', label => 'France', marketplace_id => 'EBAY_FR' }, | |
| 78 | { code => 'it', label => 'Italy', marketplace_id => 'EBAY_IT' }, | |
| 79 | { code => 'es', label => 'Spain', marketplace_id => 'EBAY_ES' }, | |
| 80 | ], | |
| 81 | signup_url => 'https://www.ebay.com/sellercenter/sell', | |
| 82 | developer_url=> 'https://developer.ebay.com/', | |
| 83 | seller_plan_required => 'Any seller account (Free)', | |
| 84 | oauth_settings => { | |
| 85 | client_id_key => 'ebay_client_id', | |
| 86 | client_secret_key => 'ebay_client_secret', | |
| 87 | runame_key => 'ebay_runame', # eBay's "RuName" identifier for the redirect URI | |
| 88 | refresh_url => 'https://api.ebay.com/identity/v1/oauth2/token', | |
| 89 | authorize_url => 'https://auth.ebay.com/oauth2/authorize', | |
| 90 | sandbox_authorize => 'https://auth.sandbox.ebay.com/oauth2/authorize', | |
| 91 | callback_cgi => '/oauth_ebay.cgi', | |
| 92 | scope => 'https://api.ebay.com/oauth/api_scope ' | |
| 93 | . 'https://api.ebay.com/oauth/api_scope/sell.inventory ' | |
| 94 | . 'https://api.ebay.com/oauth/api_scope/sell.marketing ' | |
| 95 | . 'https://api.ebay.com/oauth/api_scope/sell.account ' | |
| 96 | . 'https://api.ebay.com/oauth/api_scope/sell.fulfillment ' | |
| 97 | . 'https://api.ebay.com/oauth/api_scope/buy.browse', | |
| 98 | }, | |
| 99 | icon => 'eB', | |
| 100 | accent => '#e53238', | |
| 101 | }, | |
| 102 | { | |
| 103 | slug => 'walmart', | |
| 104 | name => 'Walmart', | |
| 105 | tagline => 'Walmart Marketplace. Consumer ID + RSA private-key signed requests.', | |
| 106 | adapter => 'MODS::RePricer::Marketplaces::Walmart', | |
| 107 | auth => 'api_key', # not OAuth -- seller pastes a Consumer ID + Private Key | |
| 108 | regions => [ | |
| 109 | { code => 'us', label => 'United States', marketplace_id => 'WALMART_US' }, | |
| 110 | { code => 'ca', label => 'Canada', marketplace_id => 'WALMART_CA' }, | |
| 111 | { code => 'mx', label => 'Mexico', marketplace_id => 'WALMART_MX' }, | |
| 112 | ], | |
| 113 | signup_url => 'https://marketplace.walmart.com/apply/', | |
| 114 | developer_url=> 'https://developer.walmart.com/home/us-mp', | |
| 115 | seller_plan_required => 'Walmart Marketplace seller (application-gated)', | |
| 116 | oauth_settings => { | |
| 117 | # No OAuth -- the "credentials" UI captures these per-account | |
| 118 | # via marketplaces.cgi instead. | |
| 119 | api_base_us => 'https://marketplace.walmartapis.com', | |
| 120 | api_base_ca => 'https://marketplace.walmartapis.com/v3/ca', | |
| 121 | }, | |
| 122 | icon => 'W', | |
| 123 | accent => '#0071dc', | |
| 124 | }, | |
| 125 | ); | |
| 126 | ||
| 127 | # Whole-list accessor for the dashboard. | |
| 128 | sub platforms { return @PLATFORMS; } | |
| 129 | ||
| 130 | # Look one up by slug. Returns undef if unknown. | |
| 131 | sub by_slug { | |
| 132 | my ($slug) = @_; | |
| 133 | foreach my $p (@PLATFORMS) { | |
| 134 | return $p if $p->{slug} eq $slug; | |
| 135 | } | |
| 136 | return undef; | |
| 137 | } | |
| 138 | ||
| 139 | # Convenience: load and instantiate the right adapter for a given | |
| 140 | # marketplace_accounts row. Returns undef on missing platform or load | |
| 141 | # failure (eval-guarded). | |
| 142 | sub adapter_for { | |
| 143 | my ($account) = @_; | |
| 144 | return undef unless $account && $account->{platform}; | |
| 145 | my $p = by_slug($account->{platform}); | |
| 146 | return undef unless $p && $p->{adapter}; | |
| 147 | my $module = $p->{adapter}; | |
| 148 | my $loaded = eval "require $module; 1"; | |
| 149 | return undef unless $loaded; | |
| 150 | return $module->new($account); | |
| 151 | } | |
| 152 | ||
| 153 | 1; |