Skip to content

Commit cb36f2e

Browse files
hc-sousacursoragent
andcommitted
feat(marketplace): seed_marketplace_demo management command
Adds `python manage.py seed_marketplace_demo` to populate ~28 published São Miguel providers with contact info, geo coords, promoted flags, and sample reviews. Supports --clear, --count, and --island. Idempotent per demo session prefix. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5f74ad7 commit cb36f2e

3 files changed

Lines changed: 187 additions & 0 deletions

File tree

src/marketplace/management/__init__.py

Whitespace-only changes.

src/marketplace/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"""Seed published marketplace listings for local dev / demo."""
2+
3+
from __future__ import annotations
4+
5+
import random
6+
7+
from django.core.management.base import BaseCommand
8+
9+
from consent.services import hash_session_id
10+
from marketplace import services
11+
from marketplace.models import Review, ServiceCategory, ServiceProvider
12+
from tenancy.models import Island
13+
from tenancy.services import for_island
14+
15+
DEMO_SESSION_PREFIX = 'demo-seed-'
16+
17+
# (name, category_slug, bio snippet, hourly_rate or None, phone, lat, lng, promoted)
18+
DEMO_PROVIDERS: list[tuple] = [
19+
('Azores Transfer Co.', 'transfers', 'Aeroporto, hotel e porto — Ponta Delgada e ilha toda.', 35, '+351912100001', 37.7411, -25.6756, True),
20+
('Taxi João Ponta Delgada', 'transfers', 'Serviço 24h na cidade e aeroporto.', 28, '+351912100002', 37.7394, -25.6687, False),
21+
('São Miguel Island Tours', 'tours', 'Full-day van tours: Sete Cidades, Furnas, Nordeste.', 45, '+351912100003', 37.8225, -25.5178, True),
22+
('Volcanic Lakes Experience', 'tours', 'Pequenos grupos à Lagoa do Fogo e Sete Cidades.', 50, '+351912100004', 37.8500, -25.7500, False),
23+
('Guide Maria — História & Natureza', 'guides', 'Guiada certificada em PT/EN/FR.', 40, '+351912100005', 37.7560, -25.6600, False),
24+
('Pedro Trekking Guide', 'guides', 'Trilhos PR e caminhadas costeiras.', 38, '+351912100006', 37.8100, -25.4200, False),
25+
('Quinta das Hortênsias', 'accommodation', 'Quartos com pequeno-almoço em Furnas.', None, '+351912100007', 37.7710, -25.3040, False),
26+
('Casa do Atlântico', 'accommodation', 'AL familiar com vista mar em Ribeira Grande.', None, '+351912100008', 37.8210, -25.5150, False),
27+
('Dive Azores — São Miguel', 'activities', 'Mergulho e baptismo na costa sul.', 55, '+351912100009', 37.7150, -25.5200, True),
28+
('Canyoning Açores', 'activities', 'Canyoning e coasteering com guias locais.', 65, '+351912100010', 37.7300, -25.4900, False),
29+
('Whale Watch São Miguel', 'activities', 'Observação de cetáceos — saídas de Ponta Delgada.', 48, '+351912100011', 37.7380, -25.6650, False),
30+
('Surf School Ribeira', 'activities', 'Aulas de surf e aluguer de pranchas.', 35, '+351912100012', 37.8150, -25.5300, False),
31+
('Restaurante O Forno — Catering', 'food', 'Catering para eventos e grupos turísticos.', 30, '+351912100013', 37.7410, -25.6700, False),
32+
('Sabores da Terra', 'food', 'Experiências gastronómicas em quintas locais.', 42, '+351912100014', 37.7800, -25.3500, False),
33+
('Rent-a-Car São Miguel', 'rentals', 'Viaturas e carrinhas sem chauffeur.', 25, '+351912100015', 37.7420, -25.6970, False),
34+
('E-Bike Azores', 'rentals', 'Aluguer de e-bikes e entrega no hotel.', 18, '+351912100016', 37.7400, -25.6720, False),
35+
('Electricista Ribeira', 'other', 'Instalações eléctricas residenciais e comerciais.', 32, '+351912100017', 37.8200, -25.5100, False),
36+
('Canalizador Furnas', 'other', 'Canalização, fugas e aquecimento.', 30, '+351912100018', 37.7700, -25.3100, False),
37+
('Fotógrafo de Casamentos Açores', 'other', 'Sessões e casamentos em locais icónicos.', 75, '+351912100019', 37.7550, -25.6800, False),
38+
('Jardim & Paisagismo Verde', 'other', 'Manutenção de jardins e relvados.', 22, '+351912100020', 37.7480, -25.6550, False),
39+
('Pet Sit São Miguel', 'other', 'Passeio e cuidados de animais à domicílio.', 15, '+351912100021', 37.7350, -25.6750, False),
40+
('Massagem & Bem-estar Furnas', 'other', 'Massagens terapêuticas pós-trilho.', 40, '+351912100022', 37.7690, -25.3050, False),
41+
('Tradutor EN-PT Juramentado', 'other', 'Traduções e apoio burocrático.', 45, '+351912100023', 37.7415, -25.6670, False),
42+
('Limpeza Doméstica Ponta Delgada', 'other', 'Limpeza regular ou one-off para AL.', 20, '+351912100024', 37.7370, -25.6620, False),
43+
('Informática & Wi-Fi AL', 'other', 'Redes, routers e suporte a alojamento local.', 35, '+351912100025', 37.7430, -25.6710, False),
44+
('Pintura & Remodelação', 'other', 'Interiores, exteriores e pequenas obras.', 28, '+351912100026', 37.7520, -25.6480, False),
45+
('Yoga ao Ar Livre — Sete Cidades', 'activities', 'Aulas matinais com vista para a lagoa.', 25, '+351912100027', 37.8450, -25.7800, False),
46+
('Pesca Desportiva São Miguel', 'activities', 'Embarcações partilhadas e privadas.', 80, '+351912100028', 37.7310, -25.6580, False),
47+
]
48+
49+
REVIEW_SNIPPETS = [
50+
('Excelente serviço, pontual e simpático.', 5),
51+
('Recomendo — muito profissional.', 5),
52+
('Boa experiência, voltaria a contratar.', 4),
53+
('Preço justo para a qualidade.', 4),
54+
('Comunicação fácil por WhatsApp.', 5),
55+
('Serviço correcto, nada a apontar.', 4),
56+
('Fantástico para turistas de primeira viagem.', 5),
57+
]
58+
59+
60+
class Command(BaseCommand):
61+
help = 'Seed ~28 published marketplace providers (and sample reviews) for demo/dev.'
62+
63+
def add_arguments(self, parser):
64+
parser.add_argument(
65+
'--island',
66+
default='sao-miguel',
67+
help='Island key (default: sao-miguel)',
68+
)
69+
parser.add_argument(
70+
'--count',
71+
type=int,
72+
default=0,
73+
help='Max providers to create (0 = all demo rows, default 28)',
74+
)
75+
parser.add_argument(
76+
'--clear',
77+
action='store_true',
78+
help='Remove providers/reviews seeded by this command first',
79+
)
80+
81+
def handle(self, *args, **options):
82+
island_key: str = options['island']
83+
try:
84+
island = Island.objects.get(key=island_key)
85+
except Island.DoesNotExist:
86+
self.stderr.write(self.style.ERROR(f'Unknown island: {island_key}'))
87+
return
88+
89+
if options['clear']:
90+
removed = self._clear_demo(island)
91+
self.stdout.write(f'Removed {removed} demo provider(s).')
92+
93+
rows = DEMO_PROVIDERS
94+
if options['count'] > 0:
95+
rows = rows[: options['count']]
96+
97+
created = 0
98+
with for_island(island):
99+
categories = {c.slug: c for c in ServiceCategory.objects.all()}
100+
if not categories:
101+
self.stderr.write(
102+
self.style.ERROR(
103+
'No categories on this island — run migrations first '
104+
'(0002_seed_default_categories).'
105+
)
106+
)
107+
return
108+
109+
for idx, row in enumerate(rows, start=1):
110+
(
111+
name,
112+
cat_slug,
113+
bio,
114+
hourly_rate,
115+
phone,
116+
lat,
117+
lng,
118+
is_promoted,
119+
) = row
120+
if cat_slug not in categories:
121+
self.stderr.write(self.style.WARNING(f'Skip {name}: unknown category {cat_slug}'))
122+
continue
123+
124+
session_id = f'{DEMO_SESSION_PREFIX}{idx}'
125+
session_hash = hash_session_id(session_id, island.key)
126+
127+
if ServiceProvider.objects.filter(
128+
island=island,
129+
name=name,
130+
created_by_session_hash=session_hash,
131+
).exists():
132+
continue
133+
134+
payload = services.create_provider(
135+
island=island,
136+
session_hash=session_hash,
137+
data={
138+
'name': name,
139+
'category_slug': cat_slug,
140+
'bio': bio,
141+
'hourly_rate': hourly_rate,
142+
'phone': phone,
143+
'whatsapp': phone,
144+
'email': f'demo{idx}@example.com',
145+
'latitude': lat,
146+
'longitude': lng,
147+
},
148+
)
149+
provider = ServiceProvider.objects.get(id=payload['id'])
150+
provider.is_promoted = is_promoted
151+
provider.status = ServiceProvider.PUBLISHED
152+
provider.save(update_fields=['is_promoted', 'status', 'updated_at'])
153+
154+
review_count = random.randint(1, 3)
155+
for r_idx in range(review_count):
156+
text, rating = random.choice(REVIEW_SNIPPETS)
157+
reviewer_session = f'{DEMO_SESSION_PREFIX}review-{idx}-{r_idx}'
158+
reviewer_hash = hash_session_id(reviewer_session, island.key)
159+
result = services.upsert_review(
160+
provider_id=provider.id,
161+
session_hash=reviewer_hash,
162+
rating=rating,
163+
text=text,
164+
)
165+
if result:
166+
review_id = result[0]['id']
167+
services.moderate_review(review_id, 'publish')
168+
169+
services.recompute_rating(provider)
170+
created += 1
171+
172+
self.stdout.write(
173+
self.style.SUCCESS(
174+
f'Seeded {created} published provider(s) on {island_key} '
175+
f'({len(rows)} demo rows available).'
176+
)
177+
)
178+
179+
def _clear_demo(self, island: Island) -> int:
180+
with for_island(island):
181+
providers = ServiceProvider.objects.filter(
182+
created_by_session_hash__startswith=DEMO_SESSION_PREFIX
183+
)
184+
count = providers.count()
185+
Review.objects.filter(provider__in=providers).delete()
186+
providers.delete()
187+
return count

0 commit comments

Comments
 (0)