Skip to content

Commit 8b9a67e

Browse files
committed
feat: ebay wip
1 parent b4ff0cf commit 8b9a67e

32 files changed

Lines changed: 1785 additions & 42 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@
2424
- **Pricing** — PokéWallet-backed Cardmarket (EUR) and TCGPlayer (USD) signals, with configurable margin.
2525
- **Inventory** — Articles CRUD, images (e.g. Supabase), sold state and sell price.
2626
- **Vinted** — Optional automated listing creation (see API docs for credentials and limitations).
27+
- **eBay** — Optional listing via eBay Inventory API (OAuth per user, business policies). Setup guide: [`api/EBAY.md`](api/EBAY.md).
2728
- **Dashboard** — Revenue, profit, and inventory-oriented views.
2829

2930
## Tech stack (overview)
3031

3132
- **Frontend:** Nuxt, Nuxt UI, Vue 3
3233
- **Backend:** FastAPI, SQLAlchemy, MariaDB/MySQL, JWT auth
33-
- **Integrations:** Groq (vision), PokéWallet (prices), optional Vinted automation
34+
- **Integrations:** Groq (vision), PokéWallet (prices), optional Vinted automation, optional eBay Sell APIs
3435

3536
For setup, environment variables, and run commands, use the dedicated guides:
3637

api/.env.example

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ VINTED_BROWSER_DISCREET_X=-2400
2020
VINTED_BROWSER_DISCREET_Y=0
2121
VINTED_BROWSER_DISCREET_MINIMIZE=true
2222
VINTED_CHROME_EXECUTABLE=
23-
# Profil Chromium persistant (session Vinted). Vide = %LOCALAPPDATA%\GoupixDex\vinted-nodriver-profile (Windows).
2423
# VINTED_USER_DATA_DIR=
25-
# true = profil jetable à chaque lancement (comportement ancien).
2624
# VINTED_BROWSER_EPHEMERAL=false
2725
CORS_ORIGINS=*
26+
# EBAY_CLIENT_ID=
27+
# EBAY_CLIENT_SECRET=
28+
# EBAY_REDIRECT_URI=https://votre-frontend/settings/marketplaces
29+
# EBAY_USE_SANDBOX=true

api/EBAY.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# eBay — configuration OAuth et mise en ligne (GoupixDex)
2+
3+
Ce document décrit comment préparer un compte [eBay Developers Program](https://developer.ebay.com/), configurer l’écran **Your eBay Sign-in Settings** (RuName / URLs de redirection), renseigner les variables d’environnement de l’API GoupixDex, puis connecter un vendeur depuis l’interface **Paramètres → Places de marché**.
4+
5+
## 1. Compte développeur et clés
6+
7+
1. Créez un compte sur [developer.ebay.com](https://developer.ebay.com/) et une **Application Keyset** (sandbox pour les tests, production quand vous êtes prêt).
8+
2. Notez l’**App ID (Client ID)** et le **Cert ID (Client Secret)** — ce ne sont pas les jetons d’accès API ; ils servent uniquement à l’échange OAuth côté serveur.
9+
10+
## 2. « Your eBay Sign-in Settings » (RuName)
11+
12+
Dans le portail développeur, configurez la page OAuth :
13+
14+
| Champ | Rôle |
15+
|--------|------|
16+
| **Display Title** | Nom affiché sur la page de consentement (ex. `GoupixDex`). |
17+
| **Your privacy policy URL** | URL accessible d’une politique de confidentialité (exigence programme développeur). |
18+
| **Your auth accepted URL** | **URL de callback OAuth** : eBay y redirige avec `?code=...&state=...` après accord. Elle doit être **identique** à la valeur `EBAY_REDIRECT_URI` / à l’URL utilisée dans l’app. Exemple : `https://votre-domaine.fr/settings/marketplaces` ou en local `http://localhost:3000/settings/marketplaces` (sandbox accepte souvent l’HTTP en dev). |
19+
| **Your auth declined URL** | Page si l’utilisateur refuse (ex. retour dashboard ou message simple). |
20+
21+
**Important :** pas de différence de slash final, de `http` vs `https`, ou de host (`localhost` vs `127.0.0.1`) entre le portail eBay, `EBAY_REDIRECT_URI` et l’URL réellement ouverte dans le navigateur.
22+
23+
## 3. Scopes utilisés par GoupixDex
24+
25+
L’API demande les scopes suivants (déjà codés côté backend) :
26+
27+
- `https://api.ebay.com/oauth/api_scope/sell.inventory` — création d’annonces (Inventory API).
28+
- `https://api.ebay.com/oauth/api_scope/sell.account` — lecture des politiques d’expédition, paiement et retours.
29+
30+
## 4. Variables d’environnement (API)
31+
32+
Ajoutez dans `api/.env` (voir `api/.env.example`) :
33+
34+
| Variable | Description |
35+
|----------|-------------|
36+
| `EBAY_CLIENT_ID` | App ID (Client ID) — **sandbox ou prod** selon `EBAY_USE_SANDBOX`. |
37+
| `EBAY_CLIENT_SECRET` | Cert ID (Client Secret). |
38+
| `EBAY_REDIRECT_URI` | **Exactement** la même URL que *Your auth accepted URL* (ex. `https://.../settings/marketplaces`). |
39+
| `EBAY_USE_SANDBOX` | `true` pour `auth.sandbox.ebay.com` / `api.sandbox.ebay.com`, `false` pour la production. |
40+
41+
Redémarrez l’API après modification.
42+
43+
## 5. Flux dans l’application web
44+
45+
1. **Paramètres → Places de marché** : activez **eBay**, enregistrez si besoin.
46+
2. Cliquez **Se connecter à eBay** : redirection vers la page de consentement eBay.
47+
3. Après acceptation, eBay renvoie vers `/settings/marketplaces?code=...&state=...` : le front envoie le `code` au backend (`POST /ebay/oauth/exchange`), qui stocke les jetons (chiffrés) sur l’utilisateur.
48+
4. Renseignez **l’ID de catégorie** eBay, la **clé d’emplacement** (`merchantLocationKey`) et les **trois politiques** (expédition, paiement, retours). Vous pouvez utiliser **Importer emplacement & politiques** si votre compte n’en a qu’une de chaque (sinon copiez les IDs depuis la réponse API ou l’interface eBay).
49+
50+
Sans ces champs, la publication est ignorée avec un message explicite.
51+
52+
## 6. Images et sandbox
53+
54+
- Les images d’annonce doivent être en **HTTPS** (ex. URLs Supabase) — exigence eBay Inventory API.
55+
- En **sandbox**, utilisez des comptes / annonces de test ; les IDs de catégories et politiques doivent exister pour le marketplace choisi (ex. `EBAY_FR`).
56+
57+
## 7. Références officielles
58+
59+
- [Authorization code grant](https://developer.ebay.com/api-docs/static/oauth-authorization-code-grant.html)
60+
- [Getting user consent](https://developer.ebay.com/api-docs/static/oauth-consent-request.html)
61+
- [Exchanging the authorization code](https://developer.ebay.com/api-docs/static/oauth-auth-code-grant-request.html)
62+
- [Inventory API — createOrReplaceInventoryItem](https://developer.ebay.com/api-docs/sell/inventory/resources/inventory_item/methods/createOrReplaceInventoryItem)
63+
- [createOffer / publishOffer](https://developer.ebay.com/api-docs/sell/inventory/resources/offer/methods/createOffer)
64+
65+
## 8. Dépannage rapide
66+
67+
- **Erreur à l’échange de code** : `redirect_uri` différent de celui enregistré chez eBay, ou `code` déjà consommé / expiré (les codes sont à usage unique et très courts).
68+
- **Erreur à la publication** : catégorie incorrecte, politiques incompatibles avec le marketplace, ou **descripteurs d’état** obligatoires pour certaines catégories cartes — consultez les messages d’erreur dans les logs API (`ebay_body`).

api/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# GoupixDex — API (Python)
22

3-
Backend for **GoupixDex**: Pokémon TCG scanning, pricing (PokéWallet), inventory articles, JWT auth, and optional Vinted automation (nodriver).
3+
Backend for **GoupixDex**: Pokémon TCG scanning, pricing (PokéWallet), inventory articles, JWT auth, optional Vinted automation (nodriver), and optional **eBay** listing (OAuth + Inventory API — see [`EBAY.md`](EBAY.md)).
44

55
## Stack
66

api/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ class AppSettings(BaseSettings):
6767
vinted_user_data_dir: str | None = None
6868
#: When ``true``: no ``user_data_dir`` (throwaway profile each launch, as before).
6969
vinted_browser_ephemeral: bool = False
70+
#: eBay REST (Inventory API + OAuth). Leave unset to disable server-side eBay features.
71+
ebay_client_id: str | None = None
72+
ebay_client_secret: str | None = None
73+
#: RuName / redirect URI registered in eBay Developer → Your eBay Sign-in Settings.
74+
ebay_redirect_uri: str | None = None
75+
#: Use sandbox API hosts (``auth.sandbox.ebay.com``, ``api.sandbox.ebay.com``).
76+
ebay_use_sandbox: bool = True
7077

7178

7279
@lru_cache

api/core/security.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,16 @@ def store_user_vinted_password(plain: str | None) -> str | None:
8787
return None
8888
stripped = plain.strip()
8989
return encrypt_vinted_credential(stripped) if stripped else None
90+
91+
92+
def store_ebay_token(plain: str | None) -> str | None:
93+
"""Encrypt OAuth token for DB storage (same Fernet key as Vinted secrets)."""
94+
if plain is None:
95+
return None
96+
stripped = plain.strip()
97+
return encrypt_vinted_credential(stripped) if stripped else None
98+
99+
100+
def decrypt_ebay_token(stored: str | None) -> str | None:
101+
"""Decrypt stored eBay OAuth token."""
102+
return decrypt_vinted_credential(stored)

api/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from config import get_settings
2020
from routes import articles as articles_routes
2121
from routes import auth as auth_routes
22+
from routes import ebay_route
2223
from routes import pricing_route
2324
from routes import scan as scan_routes
2425
from routes import settings_route
@@ -96,6 +97,7 @@ def health() -> dict[str, str]:
9697
app.include_router(users_routes.router)
9798
app.include_router(articles_routes.router)
9899
app.include_router(settings_route.router)
100+
app.include_router(ebay_route.router)
99101
app.include_router(pricing_route.router)
100102
app.include_router(stats_route.router)
101103
app.include_router(scan_routes.router)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- Services marchands (Vinted / eBay) et colonnes eBay pour articles (MariaDB / MySQL).
2+
3+
ALTER TABLE settings ADD COLUMN IF NOT EXISTS vinted_enabled TINYINT(1) NOT NULL DEFAULT 1;
4+
ALTER TABLE settings ADD COLUMN IF NOT EXISTS ebay_enabled TINYINT(1) NOT NULL DEFAULT 0;
5+
ALTER TABLE settings ADD COLUMN IF NOT EXISTS ebay_marketplace_id VARCHAR(32) NOT NULL DEFAULT 'EBAY_FR';
6+
ALTER TABLE settings ADD COLUMN IF NOT EXISTS ebay_category_id VARCHAR(32) NULL;
7+
ALTER TABLE settings ADD COLUMN IF NOT EXISTS ebay_merchant_location_key VARCHAR(64) NULL;
8+
ALTER TABLE settings ADD COLUMN IF NOT EXISTS ebay_fulfillment_policy_id VARCHAR(32) NULL;
9+
ALTER TABLE settings ADD COLUMN IF NOT EXISTS ebay_payment_policy_id VARCHAR(32) NULL;
10+
ALTER TABLE settings ADD COLUMN IF NOT EXISTS ebay_return_policy_id VARCHAR(32) NULL;
11+
12+
ALTER TABLE users ADD COLUMN IF NOT EXISTS ebay_refresh_token TEXT NULL;
13+
ALTER TABLE users ADD COLUMN IF NOT EXISTS ebay_access_token TEXT NULL;
14+
ALTER TABLE users ADD COLUMN IF NOT EXISTS ebay_access_expires_at DATETIME(6) NULL;
15+
16+
ALTER TABLE articles ADD COLUMN IF NOT EXISTS published_on_ebay TINYINT(1) NOT NULL DEFAULT 0;
17+
ALTER TABLE articles ADD COLUMN IF NOT EXISTS ebay_listing_id VARCHAR(64) NULL;
18+
ALTER TABLE articles ADD COLUMN IF NOT EXISTS ebay_published_at DATETIME(6) NULL;

api/models/article.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ class Article(Base):
4141
DateTime(timezone=True),
4242
nullable=True,
4343
)
44+
published_on_ebay: Mapped[bool] = mapped_column(
45+
Boolean(),
46+
default=False,
47+
server_default="0",
48+
nullable=False,
49+
)
50+
ebay_listing_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
51+
ebay_published_at: Mapped[dt.datetime | None] = mapped_column(
52+
DateTime(timezone=True),
53+
nullable=True,
54+
)
4455

4556
user: Mapped["User"] = relationship(back_populates="articles")
4657
images: Mapped[list["Image"]] = relationship(back_populates="article", cascade="all, delete-orphan")

api/models/margin_settings.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,27 @@
22

33
from __future__ import annotations
44

5-
from sqlalchemy import ForeignKey, Integer
5+
from sqlalchemy import Boolean, ForeignKey, Integer, String
66
from sqlalchemy.orm import Mapped, mapped_column, relationship
77

88
from models.base import Base
99

1010

1111
class MarginSettings(Base):
12-
"""Margin percentage applied to suggested resale price."""
12+
"""Margin percentage and marketplace integration toggles / eBay listing defaults."""
1313

1414
__tablename__ = "settings"
1515

1616
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
1717
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), unique=True)
1818
margin_percent: Mapped[int] = mapped_column(Integer(), default=20)
19+
vinted_enabled: Mapped[bool] = mapped_column(Boolean(), default=True)
20+
ebay_enabled: Mapped[bool] = mapped_column(Boolean(), default=False)
21+
ebay_marketplace_id: Mapped[str] = mapped_column(String(32), default="EBAY_FR")
22+
ebay_category_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
23+
ebay_merchant_location_key: Mapped[str | None] = mapped_column(String(64), nullable=True)
24+
ebay_fulfillment_policy_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
25+
ebay_payment_policy_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
26+
ebay_return_policy_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
1927

2028
user: Mapped["User"] = relationship(back_populates="margin_settings")

0 commit comments

Comments
 (0)