User journeys · personas & testable scenarios
Presentation derived from docs/parcours-utilisateurs.md. A map of every user type and their journeys as testable scenarios (entry point → steps → expected result). Two uses: product overview, and a base for user testing. Grounded in code (File.py::Class.method, Component.vue); symbols win over line numbers.
Status legend
- active reachable and working today.
- dormant coded but not reachable (nav disabled, gate never true).
- wired, no UI exists in DB + algorithm, no admin screen to drive it.
Before you test cross-cutting
- API = POST-only RPC (
/api/<Class>/<method>). Auth is a backend decorator; a GET is never authenticated (need_auth/need_adminabort(404)on GET). - Auth is opt-in: a
registerClassmethod is public unless decorated. AllPublicList.*is unauthenticated (the contributor acts with no account). - Admin failure = 404, not 403 (anti-leak). A failed admin auth looks like "not found".
- Anonymous identity =
useLocalStorage("contributor")+ a signed action token (Action.py) to cancel a contribution without an account. - Native iOS/Android app: same journeys in a webview + bridge (
nativeBridge.ts,isMobileApp()). - Testing dormant flows: admin→merchant impersonation (
AdminAPI.impersonate_merchant), or manual seed (marketplace:MarketplaceMerchant.marketplace_enabled=True+mode="marketplace").
1 · Anonymous visitor not logged in
Arrives via SEO / ads / social / magazine. Can browse everything, buy via affiliate, view a shared list and contribute without an account. Nothing requires login unless noted. Note: /i/marketplace/* does NOT imply login; only needsLogin gates certain routes.
1.1 Discovery / browsing active
| Journey | Entry | Backend + front |
|---|---|---|
| Home | / | content.py::ContentAPI.get_init_site_content · HomeAugust2023.vue (SSR, 1h cache) |
| Instant search (header) | type in search bar | search.py::SearchAPI.instant · SearchBar.vue, InstantSearch.vue |
| Results + infinite scroll | /i/marketplace/recherche?q= | products.py::ProductsAPI.search · SearchResults.vue, FacetedProductsList.vue |
| Explore "Our selections" | /i/marketplace/explore | tags.py::TagsAPI.list(highlight=true) · ProductsSearch.vue |
| Selection / tag page | /i/marketplace/selections/:slug | tags.py::TagsAPI.get · Tag.vue |
| Brand editorial page | /marque/:brand_slug | brands.py::BrandsAPI.get, getContent · BrandPresentationNew.vue |
| Brand product listing | /i/marketplace/marque/:slug | BrandsAPI.get + ProductsAPI.search · BrandPage.vue |
| Preset search | /i/marketplace/produits/:slug | ContentAPI.get_search_preset · StaticSearch.vue |
| Product page | /marketplace/produit/:slug/:code | products.py::ProductsAPI.details · ProductPage.vue |
Scenario — product page
Open /marketplace/produit/<slug>/<code> → gallery + live buy box + description + "add to list" / "contribute" CTA.
1.2 Affiliate buy click (redirect + tracking) active
Entry: "buy" button on a product page / list / selection.
Steps: full-page nav to /api/productlink/<product_id>/<from_location> (or /api/linkexternal/<hash>) → 302 to the merchant.
Expected: merchant redirect; a MerchantItemClick is logged only for an affiliate (MerchantItem) offer. Marketplace / LinkProduct offers redirect without click tracking.
1.3 Editorial (WordPress magazine + CMS) active
| Journey | Entry | Refs |
|---|---|---|
| Magazine (index / paged) | /magazine/:page? | ContentAPI.get_last_posts · media/index.vue |
| Magazine category | /magazine/:category_slug/:page? | get_last_posts_in_category · ArticleCategoryNew.vue |
| Article | /magazine/:category_slug/:slug | ContentAPI.get_post · Article.vue |
| Author page | /auteur/:slug | get_posts_from_author · Author.vue |
| Generic CMS page | /page/:slug | ContentAPI.get_page · ContentPage.vue |
| "À découvrir" / promoted | /a-decouvrir/:slug | ContentAPI.get_content (ns promoted.*) · Promoted.vue |
| City SEO | /naissances/:slug, /a-faire-en-famille/:slug | CMS villes.* / tourisme.* |
| Tools content hub | /outils, /outils/:slug | CMS outils.* · OutilsLayout.vue |
| Static pages | /informations-utiles, /contactez-nous, /comment-ca-marche, /la-cagnotte-naissance… | faq.vue, Contact.vue (→ SiteAPI.contact)… |
1.4 Tools
| Tool | Entry | Status |
|---|---|---|
| Baby clothing size guide | /guide-tailles-vetements-bebe → ToolsAPI.birthtool_get_clothes | active |
| First-names hub | /prenoms → ToolsAPI.get_top_prenoms | active |
| First-name search | /prenoms/recherche → SearchAPI.prenoms | active |
| First-name detail (AI + births curve) | /prenom/:slug → ToolsAPI.get_firstname_data · BirthTrend.vue | active |
| Milla, AI assistant | /milla-... → ai.py::AIAPI.ask · AIChat.vue | active (IP rate-limit 12/min) |
Pregnancy calculator (Grossesse.vue) | no route, no import | orphaned |
| Advent calendar | /i/marketplace/offres-calendrier-avent | ❗ needsLogin (not anonymous) |
1.5 SEO / social landings active
/:slugresolver: any bare slug resolves to a merchant page OR user list (merchant wins on collision), one round-trip; unknown → real 404.public_slug.py::PublicSlugAPI.resolve·SlugResolver.ts.- Marketing landings:
/landing/*,/liste-de-naissance-*(auto-generated, SSR).
1.6 View a shared list + contribute WITHOUT an account active
- View the list:
/:slug(parent's slug) →PublicList.get_from_user_slug·DashList.vue(read-only render),ContributionsFeed.vue. - Buy the gift yourself (self_bought): "I'm buying this" → confirm (email + name) →
list_add_contribution(delayed=true)returns an action token (localStorage) → merchant redirect.ContributeModal.vue::willBuy. - Contribute to the gift pot (card): "Contribute" → pick amount →
list_prepare_contribution→paiement_cagnottepage (Stripe) → card. Min amount 1€ (100). - Cancel by token: "Cancel" (localStorage) or link
?cancel_action=<token>→list_cancel_contribution(action_token).
1.7 Account creation / login active
Entry: /mon-compte-minipouce (target of needsLogin redirects, with ?next=).
Steps: Google, email/password (register/login), forgot-password (DSO link), magic-link ?dso_token.
2 · Logged-in parent birth-list creator
All owner APIs are class-gated (List, Profile, StripeConnect, Lists = auth_required=True). Front guard needsLogin. A user has several lists but exactly one active (creating/restoring one archives the others).
2.1–2.4 List management active
| Journey | Entry | Backend |
|---|---|---|
| Onboarding first list | /i/marketplace/inscription · UserStart.vue | ListAPI.create_list (⚠️ 500 on null/bad date, except christmas) |
| Manage lists (create/archive/restore) | .../parametres/listes · MyListsSettings.vue | create_list, get_archived_user_lists, restore_and_swap_list |
| List settings (title/date/slug/pot params/partner) | .../parametres/liste · ListSettings.vue | update_settings, set_birthdate, ProfileAPI.change_slug, invite_partner, update_many_props |
| Theme / cover / design | .../parametres/theme · ListStylesSettingsNew.vue | ListAPI.set_cover, update_many_props (design.*) |
2.5 Add a product active (3 cases)
| Case | Endpoint | Notes |
|---|---|---|
| Catalog | ListAPI.add_item_to_main(product_id) | rejects non-catalog |
| Custom gift | add_custom_item_to_main · ProductCustomAdd.vue | creates CustomProduct; visible to guests only if the pot accepts cards |
| External link | add_external_item_to_main · ProductExternalAdd.vue | creates LinkProduct, check_ugc_url_is_safe (unsafe URL → returns 0, silent fail) |
2.6–2.8 Organize · favorites · mark bought active
- Organize (modal
EditProductInList.vue): remove (remove_item_from_main), guest note (update_product_in_list), "essential" (update_product_priority), category (update_product_classification_in_list). QA: delete blocked if the product is partly funded. - Favorites: heart on a card; page
/i/marketplace/favorites.add_favorite/remove_favorite/toggle_favorite/get_favorites({own, partner}). Product-keyed, list-independent. - Mark bought / undo:
move_product_to_bought(createsContribution(state="self_bought")) /move_product_to_not_bought. QA: if a contribution is alreadypaid_to_wallet, undo does NOT delete (detachesproduct_id).
2.9–2.10 Share · dashboard · profile active
- Share: URL
{origin}/{slug}, native Web Share, QR (/qr-{slug}.png), QR by email (ProfileAPI.receive_qr_code).WebShare.vue. No dedicated social buttons (OS share sheet). - Dashboard
/i/account/dashboard·UserDashboard.vue: widgets fed by oneListAPI.get()(useDashboardData.ts). - Profile
.../parametres/profil·ProfileSettings.vue: photo, info (email read-only), password, delete =request_delete(softdisableAccount).
2.11 ⭐ Gift pot & wallet: receive and WITHDRAW money active
A multi-stage Stripe Connect flow. Money actually received = contributions with state="paid_to_wallet". Model Contribution.py, wrapper Stripe.py::UserStripeAccount.
| Step | Entry | Backend |
|---|---|---|
| a. Activate the pot (create the Connect account = the "wallet") | /i/cagnotte/creation · CreateCagnotte.vue | StripeConnectAPI.create_account_from_token |
| b. Identity check (KYC, required before payout) | /i/cagnotte/verification-identite · CagnotteKYC.vue | update_person_from_token (upload direct to uploads.stripe.com) |
| c. See received contributions | /i/mon-compte/cagnotte · CagnotteOverview.vue | ListAPI.get_cagnotte_contributions (filters paid_to_wallet) |
| d. Add an IBAN/RIB | in cagnotte-withdraw · AddIBAN.vue | add_iban_to_account |
| e. ⭐ Withdraw funds (payout) | /i/cagnotte/withdraw · CagnotteWithdraw.vue | get_balance + request_payout(bank_account_id) |
Scenario — withdraw
CagnotteOverview → "Récupérer ma cagnotte" → (redirects to KYC if not done) → Available/Soon balance → pick/add bank → "Payout of X€" → success (24–72h).
3 · Contributor friend/relative, often not logged in
Arrives via a share link. No account required: all PublicList.* is unauthenticated. A contributor is a Contributor (email+name), not a User: they never authenticate (no "create contributor account" journey).
- Open the shared list active:
/:slugor UUID link →PublicList.get_from_user_slug/get_from_list_uuid·DashList.vue. Disabled owner → error. - Open a product:
BuyModal.vueifis_marketplace_product(never true), elseContributeModal.vue(always). - Contribute to the pot (card) active: "Contribute" → amount → payment page → email + name → (anonymity, kind words, add-on) → card. Prepare:
list_prepare_contribution(state="pending", floor 100). Payment:Contribution.stripeCreateOneshotPaymentIntent()→ PI withtransfer_data.destination = parent's Connect account. Confirm: webhookviews/api.py::stripe_event→Contribution.confirm→paid_to_wallet.ContributeModal.vue→PayCagnotteWithCard.vue(+CardFormStripe.vue). - "I'm buying this gift" (self_bought) active: reserves the gift + redirects to the merchant (no money through Minipouce).
list_add_contribution(delayed=true)returns an action token (localStorage). Warning email (cancelable). DeadlineAction90 min + notify +15 min. - Cancel / edit by token active: "Cancel" (localStorage) or email link
?cancel_action=<token>→list_cancel_contribution(action_token)(JWTConfig.JWT_ACTION_KEY, methodcontribution_notify). Deletes the contribution exceptpaid_to_wallet. QA: apendingcontribution'shashidis mutable without auth (secret = the hash).
4 · Admin back-office
/a/admin, endpoints @api.need_admin / admin_required=True. No front route guard: only AdminBase.vue calls AdminAPI.check_access; security rests on the per-endpoint 404. Monochrome admin design system (admin.css, admin-*).
| Area | Entry | Key actions (all active unless noted) |
|---|---|---|
| Brands | /a/admin/brands · EditBrand.vue | set_featured_product, unindex / unindex_from_merchant, alias/consolidation (add_alias → BrandAlias), labels/logo. ⚠️ the brand editorial page is edited under Site ▸ Content (ns brand), not here. |
| Merchants | /a/admin/merchants | settings (name/slug/weight/enable-disable, update_merchant), feed onboarding (MerchantFeedEditor.vue, AI field guess), Shopify config, link matching, stats + CSV, impersonation (impersonate_merchant). |
| Products (page editor) | /a/admin/products · EditProduct.vue | update_rank, set_substitue, toggle_label, AI rewrite (rewrite_description) → update_smart_description, select_cover, additional images, product tags. Election diagnostic in the header: Best (live) / Elected (daily FK read by cards) / Source (content) — explains a card↔page gap. |
| Editorial | /a/admin/content, /tags, /menu | Lexical CMS (contents + templates), featured on Tag/Brand, menus, selections by tag, preset searches, theme covers. Magazine WordPress = read-only proxy (cache clear only). |
| Analytics | /a/admin/, /analytics-debug | dashboard (registrations, top products, hot users, stale feeds, pending payouts), ClickHouse debug, per-merchant/brand charts. |
| Mailing | /a/admin/mailing | Lexical email editor, templates, send campaign (audience filters → send_email Celery), transactional editor (AI var detection). |
| Other | /offers, /users, /payouts, /affiliaterules | offers/vouchers, users (impersonation, edit), payouts (execute_payout), affiliate link rules. |
4.8 Wired but NO UI (know this for testing)
| Lever | Backend | Status |
|---|---|---|
| BrandWeight (brand↔merchant buy boost) | Brand.py::BrandWeight, read in election | DB-only |
| MerchantPriority (content source) | Product.py::MerchantPriority (content_score) | DB-only (#219) |
| Featured product on Menu | Menu.py::Menu.featured_product_id | DB-only |
| Global ProductClassification (taxonomy) | Liste.py::ProductClassification | seeded, no admin CRUD |
misc.vue, editorplayground.vue | active | off-nav, direct URL only |
5 · Merchant business user
Hierarchy: Merchant (feed affiliate, no login) → MerchantBusiness (login, state invited/pending/active/disabled) → MarketplaceMerchant (Shopify connector). Gate auth_merchant_required=True (JWT user_type:"merchant", decoded on POST → g.merchant).
| Journey | Entry | Refs + status |
|---|---|---|
| Shopify onboarding (OAuth, embedded) | /shopify-bridge/sync?id_token=… | shopify_sync.py::ShopifySyncAPI.authenticate · SyncApp.vue · active (needs a real store) |
| Signup wizard | /i/shopify/setup · SetupAccount.vue | business.py::BusinessAPI.create_merchant → complete_profile · active (lands pending, admin flips to active) |
| Standalone login | /m/merchant/login · MerchantLogin.vue | login_with_password / login_with_google · active |
| Dashboard / analytics | /m/merchant · MerchantDashboard.vue | merchant_analytics.py::MerchantAnalyticsAPI.get_dashboard_kpis (ClickHouse + Postgres) · active |
| Browse products | /m/merchant/products · MerchantProducts.vue | MerchantAnalyticsAPI.get_products · active |
| Sales-channel curation (Shopify channel) | /m/merchant/sales-channel · MerchantSalesChannel.vue | MerchantSalesChannelAPI.publish_products (set_channel_membership, max 50) · active (needs Shopify + write_publications) |
| Storefront page + curation | /m/merchant/page, /page/products | MerchantPageAPI, MerchantShopAPI (curation product-keyed, EAN-stable; entries whose offer drops from the feed are hidden, not lost) · dormant (nav enabled:false) |
| Public storefront | minipouce.fr/<slug> (/:slug resolver) | public_slug.py::PublicSlugAPI.resolve → build_public_page · active |
| Stripe Connect (merchant payouts) | /m/merchant/stripe | StripeConnectBusinessAPI · dormant (marketplace mode) |
| Affiliate feed onboarding | admin console only | adminfeed.py::AdminFeedAPI (admin_required) · active but NOT merchant self-service |
MerchantSalesChannel (live, Shopify channel membership) vs the dormant MerchantShop storefront curation. Also minipouce.fr/<slug> (merchant storefront) is a different concept from /i/marketplace/marque/<slug> (product-brand page).6 · Marketplace buyer future dormant
The direct-purchase flow is coded but unreachable; exercising it needs a manual seed.
- Flow (theoretical): "Buy" button on the product page (
v-if is_marketplace_product && available) →MarketplaceOrdersAPI.prepare_user_order→ payment intent (Stripecapture_method="manual",transfer_data.destination=merchant) → address →finalize_order(creates the Shopify order then captures) → tracking.auth_required=True. - Why dormant:
is_marketplace_productis never true. It needs amkt_merchant_catalog_productoffer, so aMarketplaceMerchantinmode="marketplace"+marketplace_enabled=True(defaultsaffiliation/False;set_mode("marketplace")raises otherwise). → the button never renders, the list always falls back toContributeModal.
list_prepare_contributor_order calls MarketplaceOrder.CreateContributorOrder which does not exist (AttributeError) + the contributor_order_checkout route is commented out. File an issue before ever enabling marketplace; do not spend QA cycles here.What to test by persona
| Persona | Login? | Key active zones | Do NOT test (dormant) |
|---|---|---|---|
| Anonymous visitor | no | browsing, search, product page, affiliate buy, magazine/tools, view list + contribute without account, signup | advent calendar (auth), pregnancy calculator (orphaned) |
| Logged-in parent | yes | list (create/organize), 3 add types, favorites, share, dashboard/profile, gift pot + Stripe withdrawal | marketplace buy, explore_new |
| Contributor | no | view list, contribute (card), self_bought, cancel by token | marketplace buy from list (dead) |
| Admin | yes (admin) | brands, merchants, product page, editorial/CMS, analytics, mailing, users/payouts | BrandWeight, MerchantPriority, featured Menu (no UI) |
| Merchant | yes (business) | Shopify onboarding, dashboard analytics, sales-channel curation | storefront (page+products), merchant Stripe, self-service feed |
| Marketplace buyer | yes | — | everything (needs manual seed) |