Skip to content

Commit 049472c

Browse files
author
Clément VALENTIN
committed
feat: Add offer_url to energy offers and update scrapers
- Added `offer_url` field to energy offers in the database. - Updated TotalEnergies and Vattenfall scrapers to set `offer_url` for each parsed offer. - Modified price update service to handle the new `offer_url` field. - Enhanced the EnergyOffer interface in the web app to include `offer_url`. - Improved onboarding tour and page headers with data attributes for better user guidance. - Updated ALPIQ scraper URLs to point to official PDFs and ensured the provider exists in the database. - Implemented a migration script to add `offer_url` to the energy_offers table. - Enhanced the Simulator component to display detailed pricing for the new ZEN_FLEX offer type.
1 parent 6ce2230 commit 049472c

24 files changed

+733
-279
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""add offer_url to energy_offers
2+
3+
Revision ID: 006
4+
Revises: 005
5+
Create Date: 2025-12-11
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = '006'
16+
down_revision: Union[str, None] = '005'
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
# Add offer_url column to energy_offers table
23+
op.add_column('energy_offers', sa.Column('offer_url', sa.String(length=1024), nullable=True))
24+
25+
26+
def downgrade() -> None:
27+
# Remove offer_url column from energy_offers table
28+
op.drop_column('energy_offers', 'offer_url')
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
Migration: Update ALPIQ scraper URLs to use official PDFs
3+
4+
This migration ensures the ALPIQ provider exists and has the correct
5+
scraper URLs pointing to the official PDFs:
6+
- PRIX_STABLE_18.pdf: Électricité Stable -21,5% (prix fixe jusqu'au 30/11/2027)
7+
- gtr_elec_part.pdf: Électricité Stable -8% + Électricité Référence -4%
8+
"""
9+
import asyncio
10+
import uuid
11+
from datetime import datetime, UTC
12+
from sqlalchemy import text, select
13+
from src.models.database import get_db
14+
from src.models import EnergyProvider
15+
16+
17+
async def migrate():
18+
"""Create/update ALPIQ provider with official PDF URLs"""
19+
async for db in get_db():
20+
try:
21+
# Check if ALPIQ provider exists
22+
result = await db.execute(
23+
select(EnergyProvider).where(EnergyProvider.name == "ALPIQ")
24+
)
25+
provider = result.scalar_one_or_none()
26+
27+
alpiq_pdf_urls = [
28+
"https://particuliers.alpiq.fr/grille-tarifaire/particuliers/PRIX_STABLE_18.pdf",
29+
"https://particuliers.alpiq.fr/grille-tarifaire/particuliers/gtr_elec_part.pdf",
30+
]
31+
32+
if not provider:
33+
# Create ALPIQ provider
34+
provider = EnergyProvider(
35+
id=str(uuid.uuid4()),
36+
name="ALPIQ",
37+
website="https://particuliers.alpiq.fr",
38+
scraper_urls=alpiq_pdf_urls,
39+
is_active=True,
40+
)
41+
db.add(provider)
42+
print("✅ Created ALPIQ provider with PDF URLs")
43+
else:
44+
# Update scraper_urls if different
45+
current_urls = provider.scraper_urls or []
46+
if current_urls != alpiq_pdf_urls:
47+
provider.scraper_urls = alpiq_pdf_urls
48+
provider.updated_at = datetime.now(UTC)
49+
print(f"✅ Updated ALPIQ scraper_urls from {current_urls} to {alpiq_pdf_urls}")
50+
else:
51+
print("ℹ️ ALPIQ already has correct scraper_urls")
52+
53+
await db.commit()
54+
print("\n✅ Migration completed successfully")
55+
print(" ALPIQ scraper URLs:")
56+
for url in alpiq_pdf_urls:
57+
print(f" - {url}")
58+
print("\nNext step: Run the price refresh for ALPIQ:")
59+
print(" POST /api/admin/offers/refresh/ALPIQ")
60+
61+
except Exception as e:
62+
await db.rollback()
63+
print(f"❌ Migration failed: {e}")
64+
import traceback
65+
traceback.print_exc()
66+
raise
67+
finally:
68+
break
69+
70+
71+
if __name__ == "__main__":
72+
asyncio.run(migrate())

apps/api/src/models/energy_provider.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ class EnergyOffer(Base):
7979
valid_from: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) # Date from which this tariff is valid
8080
valid_to: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) # Date until which this tariff is valid (NULL = current)
8181

82+
# Link to offer page
83+
offer_url: Mapped[str | None] = mapped_column(String(1024), nullable=True) # URL to the offer page on provider's website
84+
8285
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
8386
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(UTC), nullable=False)
8487
updated_at: Mapped[datetime] = mapped_column(

apps/api/src/routers/energy_offers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ async def list_offers(
101101
"price_updated_at": o.price_updated_at.isoformat() if o.price_updated_at else None,
102102
"valid_from": o.valid_from.isoformat() if o.valid_from else None,
103103
"valid_to": o.valid_to.isoformat() if o.valid_to else None,
104+
"offer_url": o.offer_url,
104105
"is_active": o.is_active,
105106
"created_at": o.created_at.isoformat() if o.created_at else None,
106107
}

0 commit comments

Comments
 (0)