Skip to content

Integrate Supabase authentication and enhance store management#32

Closed
DDTTYYSS wants to merge 25 commits into
mainfrom
albert-oauth
Closed

Integrate Supabase authentication and enhance store management#32
DDTTYYSS wants to merge 25 commits into
mainfrom
albert-oauth

Conversation

@DDTTYYSS

@DDTTYYSS DDTTYYSS commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

This pull request introduces major improvements to multi-store (多店家) support, per-store LINE channel configuration, and secure OAuth-based authentication for the backend. The documentation (README.md) has been extensively updated to guide users through new workflows for store provisioning, authentication, and per-store LINE webhook handling. Several backend modules, environment variables, and migration scripts have been added or updated to support these changes.

Key changes:

Multi-store & LINE channel configuration

  • Added per-store LINE channel credentials (line_channel_access_token, line_channel_secret) to the store table, with a migration (f1a2b3c4d5e6). Store slug now holds the LINE webhook destination (channel user id). This enables each store to have its own LINE Messaging API configuration.
  • Introduced a new provisioning workflow: provision_stores.py script and Makefile target to automatically create/update stores from a JSON config, query LINE for channel info, and write credentials to the database. Extensive documentation added in README.md. [1] [2]
  • Updated .env.example with new variables for per-store and fallback LINE credentials, Supabase Auth, and connection URLs for various tools.

OAuth authentication & store access control

  • Implemented secure OAuth-based backend authentication using Supabase Auth JWT. The get_current_store dependency now binds the logged-in Google account to a unique store (1:1), auto-claiming on first login, and restricts API access to the bound store only. [1] [2] [3]
  • Added new endpoints and updated API docs to reflect the new single-store-per-user model. Removed the need for X-Store-Id header in frontend/backend communication; store context is now derived from the authenticated user. [1] [2]

Developer experience & infrastructure

  • Enhanced the Makefile with new targets for migration and provisioning, ensuring all commands use the correct Python environment and drivers.
  • Improved database connection URL resolution for Alembic and async tools, supporting both pooler and direct connections, with auto-rewriting logic for different environments. [1] [2]
  • Added a "bridge" Alembic revision to align local migration history with Supabase.

Codebase structure & maintainability

  • Added a new line_client.py module to encapsulate per-store LINE API client logic, falling back to global credentials if needed.
  • Updated settings management and environment variable loading to support new features and configuration patterns.

These changes significantly improve the project's support for multiple stores, security, and maintainability, while providing clear developer workflows for provisioning and managing store credentials.

Summary by CodeRabbit

  • New Features

    • OAuth staff sign-in with onboarding flow and protected routes; frontend auth context and onboarding UI
    • JSON-driven store provisioning with automated upsert and per-store LINE credential handling
    • Store-scoped backend APIs and LINE webhook routing; per-store LINE push/handler behavior
  • Documentation

    • Revised backend setup, migration, provisioning, webhook, and multi-tenant usage guidance
  • Tests

    • Added tests for provisioning, webhook routing, and authenticated /stores/me endpoint

amytzeng and others added 23 commits May 16, 2026 17:18
- Added Supabase client setup and authentication context to manage user sessions.
- Implemented ProtectedRoute component to restrict access to authenticated users.
- Updated frontend routing to include a login page and handle session state.
- Enhanced API client to include authorization tokens for requests.
- Updated backend dependencies to include PyJWT for token handling.
- Added new environment variables for Supabase URL and anon key in settings.
- Added Supabase authentication configuration to .env.example.
- Implemented authentication logic in auth.py to link users to their respective stores.
- Updated Store model to include unique constraints for owner_email and nullable owner_auth_user_id.
- Enhanced order field configuration routes to ensure users can only access their own stores.
- Modified statistics route to utilize the current store context for fetching stats.
- Added support for LINE channel credentials in the Store model.
- Implemented provisioning script to automate store creation and updates from a JSON configuration.
- Updated routes and services to ensure store-specific data handling for chat rooms, orders, and payment methods.
- Enhanced error handling for missing store configurations in webhook processing.
- Introduced new utility functions for managing database connections to Supabase, optimizing for async operations.
- Updated README and .env.example to reflect new configuration requirements for LINE integration.
- Added `display_config` JSON field to `store_order_field_config` for customizable order field visibility and order.
- Introduced new API endpoints for managing store-specific order field configurations.
- Updated order export functionality to utilize the new field configuration settings.
- Enhanced README documentation to reflect changes in order field settings and API usage.
- Added utility functions for building and formatting order field values for DOCX exports.
- Updated backend API to support OAuth-bound store access, removing the need for `X-Store-Id` in requests.
- Introduced new endpoint `GET /stores/me` to retrieve the store associated with the logged-in owner.
- Refactored frontend components to display the bound store name and adjust API calls accordingly.
- Enhanced README documentation to reflect the new authentication and store selection process.
- Added tests for the new store retrieval functionality to ensure proper access control and response structure.
- Added detailed steps for changing the store owner Gmail, including scenarios for stores with and without existing logins.
- Included important notes regarding the implications of changing the owner email and the necessity of using the `--reset-owner-binding` flag.
- Enhanced clarity on the relationship between `owner_email` and `owner_auth_user_id` for better user understanding.
@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
flower-order-bot Ignored Ignored Preview Jun 6, 2026 6:31am

@DDTTYYSS DDTTYYSS requested a review from chennjustin June 6, 2026 04:35
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Worried about impact? Review this PR in Change Stack to explore blast radius before you approve or request changes.

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a5bc9507-cf23-4dc7-b8b5-41a80b37e923

📥 Commits

Reviewing files that changed from the base of the PR and between 43d1cff and ca9ec2c.

📒 Files selected for processing (3)
  • backend/app/repositories/user_repository.py
  • backend/app/services/order_service.py
  • backend/app/services/user_service.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/app/services/user_service.py
  • backend/app/repositories/user_repository.py
  • backend/app/services/order_service.py

📝 Walkthrough

Walkthrough

Back end: Supabase JWT resolves a single owner-bound store; routes, services, and LINE handling are store-scoped. Added migrations, provisioning CLI, and per-store LINE clients. Front end: Supabase auth, onboarding flow, protected routes, and /stores/me integration; API interceptor injects bearer token.

Changes

Single-store auth, per-store LINE, provisioning, and frontend onboarding

Layer / File(s) Summary
Store model and migrations
backend/alembic/..., backend/app/models/store.py, backend/docs/manual_*.sql, backend/tests/*
Add LINE credential columns, unique owner_email, nullable owner_auth_user_id; add Alembic revisions and manual SQL; update tests.
Supabase auth, settings, and current-store binding
backend/app/core/{deps,auth,settings,supabase_db_url}.py, backend/.env.example, backend/Makefile, backend/requirements.txt, README.md
Add OAuth2 bearer processing via Supabase (get_current_user), get_current_store binding with first-login claim and concurrency handling; settings and DB URL rewrite helpers; docs and env examples updated.
Per-store LINE clients and webhook flows
backend/app/core/line_client.py, backend/app/repositories/store_repository.py, backend/app/routes/linebot.py, backend/app/usecases/*, backend/app/utils/line_*, backend/app/services/*, backend/tests/test_multi_store_line.py
Resolve webhook destinationstore.slug; create per-store LineBotApi/WebhookHandler; thread Store and LineBotApi through handlers and services; update utilities to accept injected clients; add tests and contract docs.
Store-scoped routes, services, and repo filters
backend/app/routes/*, backend/app/repositories/*, backend/app/services/*, docs/CONTRACT.md
Refactor endpoints to depend on get_current_store, enforce ownership checks, and update repository list functions to accept optional store_id; add GET /stores/me.
Store provisioning CLI & schema
backend/app/schemas/store_provision.py, backend/scripts/provision_stores.py, backend/config/*, backend/Makefile, .gitignore, backend/tests/test_store_provision.py
Add provisioning schema and CLI to upsert stores from JSON; resolve slug via LINE Bot info; support dry-run and --reset-owner-binding; provide example JSON and Make target; update ignore rule.
Frontend Supabase auth, onboarding, and protected routing
frontend/**/*, docker-compose.yml
Introduce Supabase client and AuthContext; inject JWT into API calls; replace multi-store picker with /stores/me flow; add onboarding pages, guards, layouts, routing, and refactor store/display-config contexts; add mocks and support config.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(0, 150, 255, 0.5)
  participant Browser
  participant Frontend
  participant Supabase
  end
  participant Backend
  participant DB
  participant LINE

  Browser->>Frontend: request (with stored session)
  Frontend->>Supabase: supabase.auth.getSession()
  Frontend->>Backend: API request Authorization: Bearer <token>
  Backend->>Supabase: GET /auth/user (token)
  Supabase-->>Backend: user(id,email)
  Backend->>DB: find/claim Store by owner_auth_user_id or owner_email
  DB-->>Backend: Store
  Backend-->>Frontend: Store-scoped response

  LINE->>Backend: POST /callback (events + destination)
  Backend->>DB: resolve_store_for_webhook(destination)
  DB-->>Backend: Store
  Backend->>Backend: webhook_handler_for_store(store).parse + dispatch
  Backend->>LINE: reply/push via LineBotApi(store)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

A rabbit hops to wire each store,
Tokens, slugs, and webhooks galore.
Supabase hums, migrations sing,
Provision seeds and LINE bells ring.
Onboard, protect — the garden thrives! 🐇🌸

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch albert-oauth
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch albert-oauth

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR upgrades the project to support per-store LINE channel credentials and routing (via LINE webhook destinationstore.slug), and introduces Supabase OAuth/JWT-based authentication to scope staff dashboard access to a single, authenticated store. It also adds a provisioning workflow (JSON → DB) and updates frontend routing/UI to include login + onboarding flows.

Changes:

  • Add per-store LINE credentials, multi-store webhook routing, and a store provisioning script/workflow.
  • Introduce Supabase-authenticated store binding (get_current_store) and scope staff APIs to the bound store.
  • Update frontend to use Supabase OAuth + onboarding routes, and remove the X-Store-Id model in favor of Bearer JWT.

Reviewed changes

Copilot reviewed 82 out of 84 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
README.md Updated docs for provisioning, multi-store LINE, OAuth single-store model
docs/CONTRACT.md Contract updates for destination routing + auth requirements
docker-compose.yml Frontend env wiring for Supabase config
.gitignore Ignore provisioning secrets JSON
backend/.env.example New Supabase/Auth + DB URL variables and guidance
backend/requirements.txt Add PyJWT dependency
backend/Makefile Add venv-safe targets for migrate/provision/test/run
backend/scripts/provision_stores.py Provision stores from JSON and resolve LINE slug via bot info
backend/config/stores.provision.example.json Example provisioning config
backend/docs/manual_migration_f1a2b3c4d5e6.sql Manual SQL fallback for LINE credential columns
backend/alembic/versions/a9f3c2d1e4b7_supabase_bridge_revision.py Bridge revision to align Alembic history (see review notes)
backend/alembic/versions/f1a2b3c4d5e6_add_store_line_credentials.py Migration adding per-store LINE credential columns
backend/app/core/supabase_db_url.py Helpers to rewrite Supabase pooler/direct DB URLs
backend/app/core/settings.py Settings extended for Supabase Auth + DB URL resolution
backend/app/core/deps.py Add Supabase user lookup for Bearer token validation
backend/app/core/auth.py Implement store binding + store-scoped resource guards
backend/app/core/line_client.py Per-store LINE API/WebhookHandler factory with env fallback
backend/app/models/store.py Store model updated for LINE creds + owner binding fields
backend/app/schemas/store_provision.py Pydantic schema for provisioning input
backend/app/utils/line_bot_info.py LINE bot info helper (GET /v2/bot/info) for provisioning
backend/app/utils/line_send_message.py Require passing per-store LineBotApi into send helpers
backend/app/utils/line_inbound_media.py Require passing LineBotApi for media fetch
backend/app/utils/line_get_profile.py Require passing LineBotApi for profile fetch
backend/app/repositories/store_repository.py Destination parsing + store resolution + provisioning upsert
backend/app/repositories/user_repository.py Scope user lookup by (line_uid, store_id)
backend/app/repositories/chat_repository.py Add optional store filter to chat room listing
backend/app/repositories/order_repository.py Add optional store scoping for order list queries
backend/app/repositories/payment_repository.py Add optional store scoping for payment method list
backend/app/services/user_service.py Propagate store_id into user lookup
backend/app/services/message_service.py Use per-store LINE API for staff push + store checks
backend/app/services/order_service.py Store-scoped list + per-store LINE push on order create
backend/app/services/payment_service.py Store-scoped payment method listing
backend/app/usecases/linebot_flow.py Route inbound LINE events through resolved store + per-store LINE API
backend/app/usecases/organize_order_draft.py Use per-store LINE API for warning push
backend/app/routes/linebot.py Resolve store via destination before dispatching handlers
backend/app/routes/stores.py Add GET /stores/me using get_current_store
backend/app/routes/orders.py Add store binding/scoping to several order endpoints
backend/app/routes/messages.py Enforce store scoping on chat/message endpoints
backend/app/routes/order_field_config.py Add OAuth-bound and store-id guarded config endpoints
backend/app/routes/payment.py Enforce store scoping for payment method endpoints
backend/app/routes/statistics.py Update stats endpoint to use OAuth-bound store
backend/app/routes/organize_data.py Enforce store scoping for organize endpoint
backend/app/routes/export_docx.py Enforce store scoping for docx export
backend/app/seeds/seed_user.py Update seed error message for new provisioning flow
backend/tests/test_stores_me.py Tests for /stores/me auth + binding behavior
backend/tests/test_store_provision.py Tests for provisioning schema + LINE bot info helper
backend/tests/test_multi_store_line.py Tests for destination parsing + webhook store resolution
backend/tests/test_models_pr_changes.py Update Store column expectations (LINE creds)
frontend/package.json Add @supabase/supabase-js + tslib
frontend/package-lock.json Lockfile updates for Supabase deps
frontend/src/lib/supabase.ts Supabase client initialization
frontend/src/api/client.ts Attach Bearer JWT to API requests + 401 handling
frontend/src/contexts/AuthContext.tsx Auth provider bridging Supabase session to app session
frontend/src/hooks/useAuth.ts Auth hook for context access
frontend/src/router.tsx Add /login + /onboarding routes and guards
frontend/src/pages/LoginPage.tsx Supabase Google OAuth entry + redirects
frontend/src/layouts/OnboardingLayout.tsx Onboarding shell layout
frontend/src/pages/onboarding/OnboardingNamePage.tsx Onboarding step 1 (name)
frontend/src/pages/onboarding/OnboardingLineOfficialPage.tsx Onboarding step 2 (LINE OA confirm)
frontend/src/components/onboarding/OnboardingCard.tsx Shared onboarding card UI
frontend/src/components/onboarding/StepIndicator.tsx Wizard step indicator
frontend/src/components/auth/RequireAuth.tsx Guard for authenticated routes
frontend/src/components/auth/RequireOnboardingDone.tsx Guard for onboarding completion
frontend/src/components/auth/OnboardingStepGuard.tsx Guard for correct onboarding step
frontend/src/components/auth/OnboardingIndexRedirect.tsx Redirect /onboarding → current step
frontend/src/components/auth/AuthLoading.tsx Loading placeholder
frontend/src/components/auth/ProtectedRoute.tsx Generic protected outlet (added)
frontend/src/layouts/AppLayout.tsx App shell extracted (Navbar + Outlet)
frontend/src/App.tsx Delegate to AppLayout
frontend/src/main.tsx Wrap app in AuthProvider
frontend/src/context/StoreContext.tsx Replace store picker list with OAuth-bound /stores/me
frontend/src/components/layout/StorePicker.tsx Replace selector with single store name display
frontend/src/api/stores.ts Add fetchMyStore (/stores/me)
frontend/src/api/orderFieldConfig.ts Switch to OAuth-bound /store/order-field-config/default
frontend/src/context/OrderDisplayConfigContext.tsx Remove store picker dependency and fetch OAuth-bound config
frontend/src/pages/settings/OrderFieldsPage.tsx Placeholder onboarding finale route
frontend/src/lib/onboardingPaths.ts Map onboarding step → route
frontend/src/lib/authStorage.ts Local mock staff session storage
frontend/src/lib/auth.ts Router helpers for auth redirects (future use)
frontend/src/api/auth.ts Mock auth API surface for onboarding (future backend wiring)
frontend/src/types/auth.ts Staff session/onboarding types
frontend/src/types/authApi.ts Future auth API response/body types
frontend/src/config/support.ts Support contact info for onboarding mismatch
frontend/src/config/mockLineOfficial.ts Mock LINE OA display for onboarding
Files not reviewed (1)
  • frontend/package-lock.json: Language not supported
Comments suppressed due to low confidence (2)

backend/app/routes/orders.py:42

  • GET /orders/room/{room_id} is currently unauthenticated and not scoped to the OAuth-bound store. This allows any caller to enumerate orders for any room_id (cross-tenant data exposure). It should require get_current_store and verify the chat room belongs to that store (e.g., get_chat_room_for_store) before returning data.
@api_router.get("/orders/room/{room_id}", response_model=List[OrderOut])
async def get_orders_by_room(room_id: int, db: AsyncSession = Depends(get_db)):
    return await get_orders_by_room_id(db, room_id)

backend/app/routes/orders.py:62

  • PATCH /orders/{order_id} does not require auth or enforce store ownership, so any caller can modify orders across tenants if they know an order_id. Please require get_current_store and call get_order_for_store before applying updates.
@api_router.patch("/orders/{order_id}", response_model=OrderOut)
async def patch_order(
    order_id: int,
    body: OrderPatchUpdate,
    db: AsyncSession = Depends(get_db),
):
    return await update_order_fields_by_id(db, order_id, body)


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +99 to +104
if store is not None:
if not store.active:
raise HTTPException(
status_code=status.HTTP_503_FORBIDDEN,
detail="Store is inactive",
)
Comment on lines +26 to 35
async def list_chat_rooms(db: AsyncSession, store_id: int | None = None) -> list[ChatRoom]:
stmt = (
select(ChatRoom)
.options(joinedload(ChatRoom.customer))
.where(ChatRoom.store_id == store_id)
.order_by(ChatRoom.updated_at.desc())
)
if store_id is not None:
stmt = stmt.where(ChatRoom.store_id == store_id)
result = await db.execute(stmt)
Comment thread backend/app/routes/statistics.py Outdated
Comment on lines +13 to +16
revision = "a9f3c2d1e4b7"
down_revision = "e2f1a4b5c6d7"
branch_labels = None
depends_on = None
Comment on lines +16 to +33
__table_args__ = (UniqueConstraint("owner_email", name="uq_store_owner_email"),)

id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String, nullable=False)
# LINE webhook destination (channel user id, e.g. U4b…); must match JSON "destination"
slug: Mapped[str | None] = mapped_column(String, unique=True, nullable=True)
line_channel_access_token: Mapped[str | None] = mapped_column(String, nullable=True)
line_channel_secret: Mapped[str | None] = mapped_column(String, nullable=True)
timezone: Mapped[str] = mapped_column(String, nullable=False, default="Asia/Taipei")
active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=now_taipei_naive)
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=now_taipei_naive, onupdate=now_taipei_naive
)
owner_auth_user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False)
# 管理者指派的店主 Gmail(小寫);首次登入時用來認領 store
owner_email: Mapped[str] = mapped_column(String, nullable=False)
# 首次登入時由 Supabase auth user id 綁定;未綁定前為 NULL(partial-unique 由 DB 端建立)
owner_auth_user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True)
Comment on lines +1 to +8
"""add store LINE channel credentials

Revision ID: f1a2b3c4d5e6
Revises: e2f1a4b5c6d7
Create Date: 2026-06-04

store.slug holds LINE webhook destination (channel user id).
"""
Comment on lines +17 to +24
<header className="mb-8 text-center">
<p className="m-0 text-sm font-medium tracking-[2px] text-white/80">
奇美花店
</p>
<h1 className="m-0 mt-1 text-2xl font-bold tracking-wide text-white">
Chi-Mei Floral
</h1>
<p className="m-0 mt-2 text-sm text-white/75">帳號設定</p>
Comment thread docker-compose.yml
Comment on lines +30 to +31
env_file:
- ./frontend/.env.local
Comment on lines 72 to 81
# 更新 order 狀態(店家手動標示)
@api_router.patch("/order/{order_id}/status", response_model=OrderOut)
async def update_order_status(
order_id: int,
body: OrderStatusUpdate,
store: Store = Depends(get_current_store),
db: AsyncSession = Depends(get_db),
):
await get_order_for_store(db, order_id, store)
return await update_order_status_by_id(db, order_id, body.status)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/app/routes/orders.py (2)

40-42: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

GET /orders/room/{room_id} still bypasses store authorization.

Lines 40-42 never resolve get_current_store or call get_chat_room_for_store(...), so a user who knows another store’s room_id can read its orders even though the rest of this router was moved to single-store scoping.

Proposed fix
 `@api_router.get`("/orders/room/{room_id}", response_model=List[OrderOut])
-async def get_orders_by_room(room_id: int, db: AsyncSession = Depends(get_db)):
+async def get_orders_by_room(
+    room_id: int,
+    store: Store = Depends(get_current_store),
+    db: AsyncSession = Depends(get_db),
+):
+    await get_chat_room_for_store(db, room_id, store)
     return await get_orders_by_room_id(db, room_id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/routes/orders.py` around lines 40 - 42, The GET
/orders/room/{room_id} endpoint currently calls get_orders_by_room_id directly
and skips store authorization; update the route handler get_orders_by_room to
accept the current store (via Depends(get_current_store)) and before returning
orders call get_chat_room_for_store(db, current_store.id, room_id) to validate
the room belongs to that store (raise/propagate the same not-found/forbidden
behavior if it doesn't), then call and return get_orders_by_room_id(db, room_id)
only after validation.

55-69: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

The remaining order-by-id endpoints still miss the ownership check.

patch_order(...) and suggest_order_from_chat_route(...) call downstream services with a raw order_id but never bind that ID to the authenticated store. That leaves both an unauthorized write path and an unauthorized read path after the rest of the file was hardened.

Proposed fix
 `@api_router.patch`("/orders/{order_id}", response_model=OrderOut)
 async def patch_order(
     order_id: int,
     body: OrderPatchUpdate,
+    store: Store = Depends(get_current_store),
     db: AsyncSession = Depends(get_db),
 ):
+    await get_order_for_store(db, order_id, store)
     return await update_order_fields_by_id(db, order_id, body)
 
 
 `@api_router.post`("/orders/{order_id}/suggest-from-chat", response_model=OrderSuggestFromChatOut)
 async def suggest_order_from_chat_route(
     order_id: int,
+    store: Store = Depends(get_current_store),
     db: AsyncSession = Depends(get_db),
 ):
+    await get_order_for_store(db, order_id, store)
     return await suggest_order_from_chat(db, order_id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/routes/orders.py` around lines 55 - 69, Both patch_order and
suggest_order_from_chat_route call downstream helpers with an unvalidated raw
order_id; add the same ownership check used elsewhere by fetching the
authenticated store/user (e.g., via the existing dependency used in other
routes) and either validate that the order belongs to that store or pass the
bound store_id into update_order_fields_by_id and suggest_order_from_chat so the
downstream functions enforce/store-scope the operation; specifically locate
patch_order and suggest_order_from_chat_route and modify their call sites to
include the authenticated store identifier (or perform a quick DB lookup to
assert ownership) before calling update_order_fields_by_id(order_id, ...) or
suggest_order_from_chat(order_id, ...).
🟠 Major comments (20)
frontend/src/config/support.ts-9-11 (1)

9-11: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace placeholder support channels before release.

These values are currently placeholder-only and will route users to non-functional contacts in a critical onboarding recovery path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/config/support.ts` around lines 9 - 11, The support config
currently contains placeholder values—update the email, lineId, and hours fields
in the support object (keys: email, lineId, hours) in
frontend/src/config/support.ts to real, production-ready support contacts and
business hours; replace 'support@example.com' with the official support email,
'`@example-support`' with the real Line/IM identifier or remove if not used, and
set accurate operating hours (localized string) so the onboarding recovery path
routes to functional contacts.
frontend/src/api/auth.ts-92-98 (1)

92-98: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

VITE_USE_MOCK_AUTH toggle is currently ignored.

Line 98 always exports mockAuthApi, so setting VITE_USE_MOCK_AUTH='false' has no effect. This can silently ship mock auth behavior to non-mock environments.

🔧 Proposed fix (minimum safe behavior)
 /** Active auth API (mock for now). */
-export const authApi: AuthApi = mockAuthApi
+if (!isMockAuthEnabled()) {
+  throw new Error('Real Auth API is not implemented yet, but VITE_USE_MOCK_AUTH=false')
+}
+export const authApi: AuthApi = mockAuthApi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/api/auth.ts` around lines 92 - 98, The export always uses
mockAuthApi; change authApi to be conditionally assigned using
isMockAuthEnabled(): export authApi should reference mockAuthApi when
isMockAuthEnabled() is true and fall back to the real implementation (e.g.,
realAuthApi or productionAuthApi) when false. Update the declaration of authApi
(symbol: authApi) to perform the runtime check and ensure it conforms to the
AuthApi type and that the real implementation symbol (e.g., realAuthApi) is
imported/available.
frontend/src/contexts/AuthContext.tsx-64-67 (1)

64-67: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Bind cached staff session to the Supabase user identity.

Line 64 reuses cached local session for any authenticated user. If a different Google account signs in on the same browser, previous onboarding/store state is reused incorrectly. Session cache must be keyed to supabaseSession.user.id (or cleared/recreated on mismatch).

🔧 Proposed fix (identity-bound cache)
+const STAFF_SESSION_OWNER_KEY = 'flower_auth_session_owner'
+
 function createStaffSessionFromUser(user: User): StaffSession {
@@
   setStaffSession(session)
+  localStorage.setItem(STAFF_SESSION_OWNER_KEY, user.id)
   return session
 }
@@
 function bridgeStaffSession(supabaseSession: Session | null): StaffSession | null {
@@
   const existing = getStaffSession()
-  if (existing) {
+  const ownerId = localStorage.getItem(STAFF_SESSION_OWNER_KEY)
+  if (existing && ownerId === supabaseSession.user.id) {
     return existing
   }
+  clearStaffSession()
+  localStorage.removeItem(STAFF_SESSION_OWNER_KEY)
 
   // TODO: replace with GET /auth/me when backend Google OAuth is ready.
   return createStaffSessionFromUser(supabaseSession.user)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/contexts/AuthContext.tsx` around lines 64 - 67, The cached staff
session returned by getStaffSession() is not bound to the current Supabase user,
causing state bleed between different Google accounts; update the cache logic
where getStaffSession() is used so it is keyed by supabaseSession.user.id (or
verify supabaseSession.user.id against the cached session and clear/recreate the
cache on mismatch). Specifically, when reading or writing the cached session in
AuthContext (the existing variable and getStaffSession() call), include the
supabaseSession.user.id in the cache key or add a check that compares
cached.userId (or similar) to supabaseSession.user.id and reset the cached
session if they differ. Ensure all places that persist or read the staff session
(getStaffSession(), any setStaffSession/store writes) follow the same
identity-bound keying and that onboarding/store state is cleared/recreated on
user id mismatch.
frontend/src/lib/supabase.ts-8-15 (1)

8-15: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast instead of using placeholder Supabase credentials.

Line 12 currently creates a real client even when config is missing (Line 8), which can silently route auth calls to a placeholder host and hide deployment misconfiguration. Make missing credentials a hard failure.

🔧 Proposed fix
 if (!isSupabaseConfigured) {
-  console.error('Missing VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY')
+  throw new Error('Missing VITE_SUPABASE_URL or VITE_SUPABASE_ANON_KEY')
 }
 
 export const supabase = createClient(
-  supabaseUrl ?? 'https://placeholder.supabase.co',
-  supabaseAnonKey ?? 'placeholder',
+  supabaseUrl!,
+  supabaseAnonKey!,
   {
     auth: {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/lib/supabase.ts` around lines 8 - 15, The code currently logs
missing Supabase config but still instantiates supabase with placeholders;
change this to fail fast by throwing an error (or exiting) when
isSupabaseConfigured is false so no createClient call runs with superset
placeholders. Locate the isSupabaseConfigured check and the export const
supabase = createClient(...) and replace the placeholder-instantiation path with
a hard failure that includes supabaseUrl/supabaseAnonKey context (or a clear
message) so createClient is only called when both supabaseUrl and
supabaseAnonKey are present.
frontend/src/context/OrderDisplayConfigContext.tsx-122-123 (1)

122-123: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fallback key misses existing per-store local cache on fetch failure.

When the first authenticated fetch fails, fallbackKey becomes order-display-config:unknown, which cannot match keys written via buildOrderDisplayStorageKey(remote.store_id). This makes the “use local cache” branch load defaults instead of the previously cached store config.

Suggested fix
-        const fallbackKey = storageKey ?? 'order-display-config:unknown'
-        const fromLocal = mergeWithRegistry(loadConfig(fallbackKey))
+        const fromLocal = mergeWithRegistry(
+          storageKey ? loadConfig(storageKey) : loadConfig(),
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/context/OrderDisplayConfigContext.tsx` around lines 122 - 123,
The fallbackKey currently defaults to 'order-display-config:unknown' which
prevents matching per-store cached keys; change the fallbackKey computation to
prefer an actual per-store key by using
buildOrderDisplayStorageKey(remote?.store_id) when storageKey is undefined (e.g.
storageKey ?? buildOrderDisplayStorageKey(remote?.store_id) ??
'order-display-config:unknown'), then pass that key into
loadConfig/mergeWithRegistry so loadConfig can find the store-specific local
cache; ensure remote is referenced safely (optional chaining) so this still
falls back to 'unknown' if no store_id is available.
frontend/src/router.tsx-42-44 (1)

42-44: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict /settings/order-fields to the intended onboarding step only.

Line 42 currently treats every non-DONE state the same, so a user in NAME can jump directly to /settings/order-fields and bypass required onboarding steps. Gate only LINE_OA here, and redirect earlier steps to their canonical onboarding route.

💡 Proposed fix
 import { useAuth } from '`@/hooks/useAuth`'
+import { getOnboardingPath } from '`@/lib/onboardingPaths`'
@@
 function OrderFieldsRoute() {
   const { session, isLoading } = useAuth()
@@
-  if (session.onboardingStep !== 'DONE') {
+  if (session.onboardingStep === 'LINE_OA') {
     return <OrderFieldsPage />
   }
 
+  if (session.onboardingStep !== 'DONE') {
+    return <Navigate to={getOnboardingPath(session.onboardingStep)} replace />
+  }
+
   return <OrderFieldSettingsPage />
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/router.tsx` around lines 42 - 44, The current guard in the
router checks session.onboardingStep !== 'DONE' and returns OrderFieldsPage,
which lets earlier steps (e.g., 'NAME') access /settings/order-fields; change
this to only allow OrderFieldsPage when session.onboardingStep === 'LINE_OA' and
otherwise redirect users in earlier states to their canonical onboarding route
(map other onboardingStep values to their respective onboarding pages and return
that route/component instead). Locate and update the condition around
session.onboardingStep and OrderFieldsPage so only 'LINE_OA' shows
OrderFieldsPage, and implement redirects for other steps.
docker-compose.yml-30-31 (1)

30-31: ⚠️ Potential issue | 🟠 Major

Avoid startup dependency on uncommitted ./frontend/.env.local.

docker-compose.yml configures the frontend service with:

env_file:
  - ./frontend/.env.local

The repo doesn’t include frontend/.env.local, and .gitignore ignores **/.env* / *.local, so fresh clones/CI commonly fail during docker compose up. Use a committed frontend/.env.example + copy/init step in the image/entrypoint, or pass required values via environment (with sensible defaults).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.yml` around lines 30 - 31, The frontend service in
docker-compose.yml is configured to load an uncommitted .env.local file which
causes startup failures in fresh clones and CI environments. Replace the
env_file directive that references ./frontend/.env.local with one of two
approaches: either create a committed ./frontend/.env.example file and add an
entrypoint or Dockerfile step that copies and initializes it for the frontend
service, or move the environment variable declarations directly into the
frontend service's environment section in docker-compose.yml with sensible
defaults so the service can start without external file dependencies.
frontend/src/pages/onboarding/OnboardingNamePage.tsx-37-40 (1)

37-40: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Await profile update before routing to next onboarding step.

At Line 38, updateDisplayName(trimmed) is not awaited, so async failures won’t be caught by this try/catch, and navigation can occur before persistence completes.

Suggested fix
-  function handleSubmit(event: FormEvent<HTMLFormElement>) {
+  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
     event.preventDefault()
     setError(null)
@@
     try {
-      updateDisplayName(trimmed)
+      await updateDisplayName(trimmed)
       navigate('/onboarding/line-official', { replace: true })
     } catch (err) {
       setError(err instanceof Error ? err.message : '無法儲存名稱')
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/onboarding/OnboardingNamePage.tsx` around lines 37 - 40,
The call to updateDisplayName(trimmed) is missing an await so async errors
escape the try/catch and navigation via navigate('/onboarding/line-official', {
replace: true }) can happen before the update completes; change the flow in
OnboardingNamePage by awaiting updateDisplayName(trimmed) inside the try block
(or handling its returned Promise with .then/.catch) and only call navigate
after the awaited update resolves, keeping the existing try/catch to surface
errors from updateDisplayName.
backend/app/models/store.py-33-33 (1)

33-33: ⚠️ Potential issue | 🟠 Major

Add DB-level unique/partial-unique enforcement for store.owner_auth_user_id (auth flow currently assumes nullable).

Alembic history defines the owner_auth_user_id column (e.g., backend/alembic/versions/f4e8bb2a9031_multitenant_schema.py) but does not create any unique/partial-unique index/constraint for non-null store.owner_auth_user_id; the only explicit uniqueness in the model is uq_store_owner_email. Also, the migration sets owner_auth_user_id as nullable=False, while backend/app/models/store.py marks it nullable=True and backend/app/core/auth.py relies on Store.owner_auth_user_id IS NULL to bind the first-login store.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/models/store.py` at line 33, Model and migrations are
inconsistent: owner_auth_user_id is nullable in Store (owner_auth_user_id) but
migration made it non-null and no DB uniqueness exists for non-null owners; add
a DB-level unique constraint/index for non-null owner_auth_user_id and make
migrations and model consistent with the auth flow. Update the model (Store) to
keep owner_auth_user_id nullable=True, then add a new Alembic migration that
creates a partial unique index (e.g., name it uq_store_owner_auth_user_id) on
owner_auth_user_id WHERE owner_auth_user_id IS NOT NULL (or the DB-specific
equivalent), and ensure the migration does not set nullable=False; finally
reference uq_store_owner_email to avoid name collisions and run the migration to
enforce uniqueness at the DB level used by auth.py checks.
backend/app/core/deps.py-39-54 (1)

39-54: ⚠️ Potential issue | 🟠 Major

Differentiate Supabase upstream failures from invalid-token responses; narrow the httpx exception scope.

backend/app/core/deps.py get_current_user currently maps any non-200 Supabase response to 401 "Invalid or expired token", so upstream 429/5xx become invalid-token errors. It also catches all exceptions (except Exception), which can mask non-network defects as 503.

🔧 Suggested fix
 async def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
     settings = get_settings()
+    if not settings.supabase_url or not settings.supabase_anon_key:
+        raise HTTPException(status_code=503, detail="Auth service misconfigured")
     try:
-        async with httpx.AsyncClient() as client:
+        async with httpx.AsyncClient(timeout=5.0) as client:
             resp = await client.get(
                 f"{settings.supabase_url}/auth/v1/user",
                 headers={
                     "Authorization": f"Bearer {token}",
                     "apikey": settings.supabase_anon_key,
                 },
-                timeout=5.0,
             )
-    except Exception:
-        raise HTTPException(status_code=503, detail="Auth service unavailable")
-    if resp.status_code != 200:
+    except httpx.RequestError as err:
+        raise HTTPException(status_code=503, detail="Auth service unavailable") from err
+
+    if resp.status_code in (401, 403):
         raise HTTPException(status_code=401, detail="Invalid or expired token")
+    if resp.status_code >= 500:
+        raise HTTPException(status_code=503, detail="Auth service unavailable")
+    if resp.status_code != 200:
+        raise HTTPException(status_code=502, detail="Auth service error")
     return resp.json()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/core/deps.py` around lines 39 - 54, In get_current_user, narrow
the broad except and distinguish upstream failures from actual invalid tokens:
catch httpx.RequestError (or httpx.HTTPError subclasses related to
network/timeouts) and raise HTTPException(status_code=503, detail="Auth service
unavailable"), then after the client.get examine resp.status_code and only raise
HTTPException(status_code=401, detail="Invalid or expired token") when
resp.status_code == 401; for 429 or any 5xx return raise
HTTPException(status_code=503, detail=f"Auth service error
({resp.status_code})") so upstream rate-limit/server errors are surfaced
correctly.

Source: Linters/SAST tools

backend/config/stores.provision.example.json-5-5 (1)

5-5: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace real email with a neutral placeholder in sample config.

The example file currently includes a specific personal-looking email address; sample provisioning templates should avoid embedding identifiable real addresses.

💡 Suggested fix
-      "owner_email": "1ddttyyss@gmail.com",
+      "owner_email": "owner@example.com",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/config/stores.provision.example.json` at line 5, The sample config
sets owner_email to a real-looking address ("1ddttyyss@gmail.com"); replace the
value of the "owner_email" key in the example provisioning JSON with a neutral
placeholder such as "owner@example.com" or "user@example.com" to avoid embedding
identifiable personal data in sample templates.
backend/app/schemas/store_provision.py-15-25 (1)

15-25: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject blank required fields after trimming.

Whitespace-only values currently validate to "" for required fields, so invalid owner_email / LINE credentials can pass schema validation and fail later in provisioning/runtime.

💡 Suggested fix
 class StoreProvisionEntry(BaseModel):
@@
-    `@field_validator`("name", "owner_email", "line_channel_access_token", "line_channel_secret", "slug")
+    `@field_validator`("name", "owner_email", "line_channel_access_token", "line_channel_secret")
     `@classmethod`
-    def strip_strings(cls, v: str | None) -> str | None:
-        if v is None:
-            return None
-        return v.strip()
+    def normalize_required_strings(cls, v: str) -> str:
+        value = v.strip()
+        if not value:
+            raise ValueError("must not be blank")
+        return value
+
+    `@field_validator`("slug")
+    `@classmethod`
+    def normalize_slug(cls, v: str | None) -> str | None:
+        if v is None:
+            return None
+        value = v.strip()
+        return value or None
@@
     `@field_validator`("owner_email")
     `@classmethod`
     def lowercase_email(cls, v: str) -> str:
-        return v.strip().lower()
+        return v.lower()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/schemas/store_provision.py` around lines 15 - 25, The current
validators strip_strings and lowercase_email allow whitespace-only inputs to
become empty strings which then pass validation; update these field validators
(registering on fields name, owner_email, line_channel_access_token,
line_channel_secret, slug and owner_email specifically) to trim the input and
raise a ValueError when the resulting string is empty (e.g., after v.strip() if
not v: raise ValueError("...")), and ensure lowercase_email still returns a
lowercased, trimmed value (and raises if empty) so blank/whitespace-only
required fields are rejected during schema validation.
backend/app/services/order_service.py-181-186 (1)

181-186: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t let LINE push failures break order creation response.

Lines 182-185 can raise (missing LINE creds/API failure) after the order commit at Line 176, causing a 500 on a partially successful flow.

Suggested fix
     if line_uid:
-        store = await get_store_by_id(db, room.store_id)
-        if store:
-            line_api = line_bot_api_for_store(store)
-            LINE_push_message(line_api, line_uid, ChatMessagePayload(text=msg))
+        try:
+            store = await get_store_by_id(db, room.store_id)
+            if store:
+                line_api = line_bot_api_for_store(store)
+                LINE_push_message(line_api, line_uid, ChatMessagePayload(text=msg))
+        except Exception:
+            # Keep order flow successful even if notification fails.
+            pass
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/order_service.py` around lines 181 - 186, The LINE push
call after order commit (the block using get_store_by_id,
line_bot_api_for_store, and LINE_push_message with ChatMessagePayload) can raise
and currently will turn a successful order into a 500; wrap the entire
post-commit LINE notification sequence in a try/except (or equivalent
error-handling for async) so exceptions are caught, log the failure with context
(store_id, line_uid, exception) and do not re-raise or affect the HTTP response;
alternatively, dispatch the notification to a background task/worker to decouple
failures from order creation.
backend/app/repositories/store_repository.py-114-121 (1)

114-121: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove destination→first-store fallback to prevent cross-store webhook routing.

Lines 114-121 route unknown destinations to the first store whenever an env secret exists. That can mis-attribute events to the wrong tenant.

Suggested fix
-    settings: Settings = load_settings()
-    secret = (settings.line_channel_secret or "").strip()
-    if secret:
-        store_id = await get_first_store_id(db)
-        if store_id is not None:
-            fallback = await get_store_by_id(db, store_id)
-            if fallback is not None:
-                return fallback
-
     raise HTTPException(
         status_code=status.HTTP_404_NOT_FOUND,
         detail=f"No store for LINE destination: {destination}",
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/repositories/store_repository.py` around lines 114 - 121, The
code is returning the first store when a line_channel_secret exists (using
load_settings, secret, get_first_store_id, get_store_by_id) which enables
cross-store webhook routing; remove the fallback branch that fetches
get_first_store_id and returns fallback (i.e. delete the if secret →
get_first_store_id → get_store_by_id → return fallback block) so that unknown
destinations are not silently mapped to the first store and the function returns
None (or propagates the original no-match behavior) instead; ensure any callers
handle a None result and update tests that assumed the fallback.
backend/app/usecases/organize_order_draft.py-176-180 (1)

176-180: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard warning push so organize flow doesn’t fail on notification errors.

Lines 177-180 can throw on missing store LINE credentials or LINE API errors, which aborts the organize flow instead of just skipping notification.

Suggested fix
         line_uid = await get_line_uid_by_chatroom_id(db, chat_room.id)
         if line_uid:
-            store = await get_store_by_id(db, chat_room.store_id)
-            if store:
-                line_api = line_bot_api_for_store(store)
-                LINE_push_message(line_api, line_uid, ChatMessagePayload(text=warning_msg))
+            try:
+                store = await get_store_by_id(db, chat_room.store_id)
+                if store:
+                    line_api = line_bot_api_for_store(store)
+                    LINE_push_message(line_api, line_uid, ChatMessagePayload(text=warning_msg))
+            except Exception:
+                pass
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/usecases/organize_order_draft.py` around lines 176 - 180, The
notification call can raise and abort the organize flow; wrap the LINE
notification in a safe guard: after retrieving store via get_store_by_id(db,
chat_room.store_id) and constructing line_api with
line_bot_api_for_store(store), verify the store has valid LINE credentials (or
that line_api is truthy) and invoke LINE_push_message(line_api, line_uid, ...)
inside a try/except block that catches API/connection errors, logs them, and
suppresses them so the organize flow continues. Ensure you reference
get_store_by_id, line_bot_api_for_store, and LINE_push_message when applying the
change.
backend/app/repositories/store_repository.py-81-81 (1)

81-81: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate webhook payload shape before accessing destination.

Line 81 assumes json.loads returns a dict. A valid JSON array/string causes AttributeError and a 500 instead of a 400.

Suggested fix
 def parse_webhook_destination(body_str: str) -> str:
     try:
         payload = json.loads(body_str)
     except json.JSONDecodeError as e:
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail="Invalid webhook JSON",
         ) from e
+    if not isinstance(payload, dict):
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail="Invalid webhook JSON",
+        )
     destination = (payload.get("destination") or "").strip()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/repositories/store_repository.py` at line 81, After json.loads
parses the webhook body, ensure the resulting payload is a dict before calling
payload.get(...)—replace the blind access at the line with "destination =
(payload.get('destination') or '').strip()" by first checking
isinstance(payload, dict); if it's not a dict, return/raise a 400 Bad Request
(or the repository's equivalent) with a clear message about expecting an object,
log the malformed payload, and only then extract destination. Reference the
payload variable and the exact assignment "destination =
(payload.get('destination') or '').strip()" when making the change.
backend/app/repositories/payment_repository.py-9-14 (1)

9-14: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make store_id mandatory to prevent accidental cross-store reads.

Allowing store_id=None returns all payment methods. That creates a tenant-isolation gap if any caller omits the argument. Make this repository API store-scoped by default, and keep global listing in a separate explicit admin-only method if needed.

Proposed fix
-async def list_payment_methods(
-    db: AsyncSession, store_id: int | None = None
-) -> list[PaymentMethod]:
+async def list_payment_methods(
+    db: AsyncSession, store_id: int
+) -> list[PaymentMethod]:
     stmt = select(PaymentMethod)
-    if store_id is not None:
-        stmt = stmt.where(PaymentMethod.store_id == store_id)
+    stmt = stmt.where(PaymentMethod.store_id == store_id)
     result = await db.execute(stmt)
     return result.scalars().all()
# Also align the service contract:
# backend/app/services/payment_service.py
# async def get_all_payment_methods(db: AsyncSession, store_id: int) -> list[PaymentMethodBase]:
#     ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/repositories/payment_repository.py` around lines 9 - 14, The
repository function list_payment_methods currently allows store_id to be omitted
which can return all payment methods; change its API to require a store_id (make
the parameter non-optional in list_payment_methods) and remove the default None
handling (drop the if store_id is not None branch), update any callers and the
service contract (e.g., payment_service.get_all_payment_methods →
get_all_payment_methods(db: AsyncSession, store_id: int) or similar) to pass a
store_id, and create a separate explicit admin-only method if global listing is
needed; ensure types/signatures and any docstrings/tests are updated to reflect
the required store-scoped parameter.
backend/app/utils/line_send_message.py-62-94 (1)

62-94: ⚠️ Potential issue | 🟠 Major

Handle LINE reply failures in send_quick_reply_message/send_confirm to prevent webhook 500s

send_quick_reply_message and send_confirm call line_bot_api.reply_message(...) without catching LineBotApiError, and /callback in backend/app/routes/linebot.py doesn’t intercept these exceptions—so transient LINE API errors can propagate and fail the request path. Add localized handling around reply_message (mirroring LINE_push_message’s LineBotApiError handling) and return a success boolean.

Suggested fix
-def send_quick_reply_message(
+def send_quick_reply_message(
     line_bot_api: LineBotApi, reply_token: str, text: str, options: list[str]
-):
+) -> bool:
     items = [
         QuickReplyButton(action=MessageAction(label=opt, text=opt))
         for opt in options
     ]
-    line_bot_api.reply_message(
-        reply_token,
-        TextSendMessage(text=text, quick_reply=QuickReply(items=items)),
-    )
+    try:
+        line_bot_api.reply_message(
+            reply_token,
+            TextSendMessage(text=text, quick_reply=QuickReply(items=items)),
+        )
+        return True
+    except LineBotApiError as e:
+        logging.error(f"[LINE REPLY] quick reply failed: {e.status_code} - {e.error.message}")
+        return False
 
 
 def send_confirm(
@@
-):
+) -> bool:
@@
-    line_bot_api.reply_message(
-        reply_token,
-        TemplateSendMessage(alt_text=text, template=tpl),
-    )
+    try:
+        line_bot_api.reply_message(
+            reply_token,
+            TemplateSendMessage(alt_text=text, template=tpl),
+        )
+        return True
+    except LineBotApiError as e:
+        logging.error(f"[LINE REPLY] confirm failed: {e.status_code} - {e.error.message}")
+        return False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/utils/line_send_message.py` around lines 62 - 94, Wrap the calls
to line_bot_api.reply_message in both send_quick_reply_message and send_confirm
with a try/except that catches LineBotApiError (same handling as
LINE_push_message): log the error (including exception details) and return False
on exception, otherwise return True on success; ensure you import
LineBotApiError if not present and preserve existing logging behavior/format
used by LINE_push_message so webhook `/callback` won't raise on transient LINE
API failures.
backend/app/utils/line_get_profile.py-5-8 (1)

5-8: ⚠️ Potential issue | 🟠 Major

Avoid blocking LINE SDK call inside async function

backend/app/utils/line_get_profile.py defines async fetch_user_profile, but it calls the synchronous line_bot_api.get_profile(...) directly (network I/O). Since backend/app/usecases/linebot_flow.py awaits this helper, it can block the event loop under load. Offload it (or use the SDK’s async client).

Suggested fix
+import asyncio
 from linebot import LineBotApi
 from linebot.exceptions import LineBotApiError
 
 
 async def fetch_user_profile(line_bot_api: LineBotApi, user_id: str):
     try:
-        profile = line_bot_api.get_profile(user_id)
+        profile = await asyncio.to_thread(line_bot_api.get_profile, user_id)
         return profile
     except LineBotApiError as e:
         print(f"Error: {e.status_code} {e.error.message}")
         return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/utils/line_get_profile.py` around lines 5 - 8, The async function
fetch_user_profile currently calls the synchronous line_bot_api.get_profile
which performs network I/O and can block the event loop; change
fetch_user_profile to offload that blocking call to a thread (e.g., use
asyncio.to_thread or loop.run_in_executor) or switch to the SDK’s async client,
so the call to line_bot_api.get_profile (or its async equivalent) does not run
on the event loop thread; update callers (e.g., those in linebot_flow.py) to
continue awaiting the async fetch_user_profile as before.
backend/app/repositories/chat_repository.py-27-34 (1)

27-34: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the unconditional store filter.

Line 30 already applies ChatRoom.store_id == store_id, so store_id=None becomes WHERE store_id IS NULL instead of “no store filter.” That breaks the new optional contract now exposed by get_chat_room_list(...) and will return the wrong rooms whenever a caller omits store_id.

Proposed fix
 async def list_chat_rooms(db: AsyncSession, store_id: int | None = None) -> list[ChatRoom]:
     stmt = (
         select(ChatRoom)
         .options(joinedload(ChatRoom.customer))
-        .where(ChatRoom.store_id == store_id)
         .order_by(ChatRoom.updated_at.desc())
     )
     if store_id is not None:
         stmt = stmt.where(ChatRoom.store_id == store_id)
     result = await db.execute(stmt)
     return result.scalars().all()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/repositories/chat_repository.py` around lines 27 - 34, The query
always applies ChatRoom.store_id == store_id when building stmt, causing
store_id=None to become WHERE store_id IS NULL; remove that unconditional
.where(...) so the base stmt uses only
select(ChatRoom).options(joinedload(ChatRoom.customer)).order_by(ChatRoom.updated_at.desc()),
and keep the conditional filter inside the if store_id is not None block (so
only apply ChatRoom.store_id == store_id when a store_id is passed); update the
code in get_chat_room_list where stmt is constructed and the stmt variable is
modified accordingly.
🟡 Minor comments (11)
frontend/src/pages/onboarding/OnboardingLineOfficialPage.tsx-30-37 (1)

30-37: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle auth action exceptions before routing.

confirmLineOfficial() can throw when no active session; this click path currently has no fallback and can fail with an unhandled error instead of redirecting cleanly.

Proposed patch
 function handleConfirmCorrect() {
-  confirmLineOfficial()
-  navigate('/settings/order-fields', { replace: true })
+  try {
+    confirmLineOfficial()
+    navigate('/settings/order-fields', { replace: true })
+  } catch {
+    navigate('/login?error=session_expired', { replace: true })
+  }
 }
 
 function handleRejectWrongAccount() {
-  rejectWrongAccount()
-  navigate('/login?error=wrong_account', { replace: true })
+  try {
+    rejectWrongAccount()
+  } finally {
+    navigate('/login?error=wrong_account', { replace: true })
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/onboarding/OnboardingLineOfficialPage.tsx` around lines 30
- 37, The click handlers call confirmLineOfficial() and rejectWrongAccount()
without guarding against exceptions which can abort navigation; wrap the calls
in try/catch (and await them if they return a Promise) inside
handleConfirmCorrect and handleRejectWrongAccount so any thrown errors are
caught/logged and navigation via navigate('/settings/order-fields', { replace:
true }) and navigate('/login?error=wrong_account', { replace: true }) always
runs as the fallback; reference the functions handleConfirmCorrect,
confirmLineOfficial, handleRejectWrongAccount, rejectWrongAccount, and navigate
when making this change.
frontend/src/lib/authStorage.ts-12-19 (1)

12-19: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate role in the runtime session guard.

isStaffSession currently accepts objects without role, so malformed localStorage payloads can be treated as valid sessions.

🔧 Proposed fix
   return (
     typeof s.staffId === 'number' &&
     typeof s.storeKey === 'string' &&
     typeof s.displayName === 'string' &&
+    typeof s.role === 'string' &&
     (s.onboardingStep === 'NAME' ||
       s.onboardingStep === 'LINE_OA' ||
       s.onboardingStep === 'DONE')
   )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/lib/authStorage.ts` around lines 12 - 19, The runtime session
guard is missing validation for the role property, so isStaffSession can accept
objects without role; update isStaffSession to also check that s.role exists and
is one of the allowed staff roles (e.g., a non-empty string or specific enum
values your app uses) along with existing checks for staffId, storeKey,
displayName, and onboardingStep; locate the isStaffSession function in
authStorage.ts and add the role validation to the boolean expression to reject
malformed localStorage payloads.
frontend/src/contexts/AuthContext.tsx-160-163 (1)

160-163: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep auth flags consistent in resetMockAuth.

Line 160 clears session but leaves isAuthenticated unchanged. This can leave guards in an inconsistent state (isAuthenticated === true with session === null).

🔧 Proposed fix
   const resetMockAuth = useCallback(() => {
     resetMockAuthStorage()
     setSession(null)
+    setIsAuthenticated(false)
   }, [])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/contexts/AuthContext.tsx` around lines 160 - 163, The
resetMockAuth function clears storage and session but does not update the
authentication flag, leaving isAuthenticated inconsistent; update resetMockAuth
(which calls resetMockAuthStorage and setSession) to also call
setIsAuthenticated(false) (and clear any related user/token state if present) so
that isAuthenticated and session remain consistent after reset.
frontend/src/components/auth/OnboardingStepGuard.tsx-27-29 (1)

27-29: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid blank-screen fallback for unauthenticated users.

At Line 27, returning null can strand users on an empty page when auth resolves to unauthenticated. Redirect to /login instead for deterministic behavior.

Suggested fix
-  if (!session) {
-    return null
-  }
+  if (!session) {
+    return <Navigate to="/login" replace />
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/auth/OnboardingStepGuard.tsx` around lines 27 - 29,
The component's session check in OnboardingStepGuard currently returns null when
no session, causing a blank screen; replace that fallback with a deterministic
redirect to the login page. Update the check in the OnboardingStepGuard
component (where `if (!session) { return null }` appears) to trigger a
client-side redirect to "/login" (e.g., using your router's replace/push or a
<Navigate/> equivalent) and return null only after initiating the redirect to
avoid rendering a blank page; ensure you import and use the app's routing helper
consistently with the rest of the app.
README.md-287-287 (1)

287-287: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix TOC anchor drift after heading rename.

The section title changed, but the existing TOC link still points to the old anchor text, so in-page navigation will break.

Suggested fix
-## 後台 API 與欄位設定(OAuth 單店)
+## 多店後台 API 與欄位設定(OAuth 單店)

or update the TOC entry to the new anchor generated from this heading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 287, TOC link pointing to the old anchor no longer matches
the renamed heading "## 後台 API 與欄位設定(OAuth 單店)"; update the corresponding entry
in the table of contents to the new anchor generated from this heading (convert
to lowercase, remove/normalize punctuation and full-width characters, replace
spaces with hyphens) so the in-page link matches the heading text and navigation
works; locate the TOC entry referencing the old anchor and replace it with the
new anchor derived from "後台 API 與欄位設定(OAuth 單店)".
backend/app/models/store.py-30-32 (1)

30-32: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resolve Ruff RUF003 ambiguous Unicode punctuation in comments.

These comment characters are flagged and can cause lint noise/failures in stricter CI.

Suggested fix
-    # 管理者指派的店主 Gmail(小寫);首次登入時用來認領 store
+    # 管理者指派的店主 Gmail (小寫); 首次登入時用來認領 store
@@
-    # 首次登入時由 Supabase auth user id 綁定;未綁定前為 NULL(partial-unique 由 DB 端建立)
+    # 首次登入時由 Supabase auth user id 綁定; 未綁定前為 NULL (partial-unique 由 DB 端建立)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/models/store.py` around lines 30 - 32, The comments containing
Chinese text in the owner_email field definition use full-width Unicode
punctuation characters (such as ()for parentheses and ;for semicolon) which
trigger the Ruff RUF003 linting rule for ambiguous Unicode punctuation. Replace
all full-width punctuation marks in these comments with their standard ASCII
equivalents: replace ( with (, replace ) with ), and replace ; with ; to resolve
the linting issue.

Source: Linters/SAST tools

backend/alembic/versions/f1a2b3c4d5e6_add_store_line_credentials.py-4-4 (1)

4-4: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix migration header revision mismatch.

Revises: in the docstring does not match down_revision, which can mislead manual migration troubleshooting.

Suggested fix
-Revises: e2f1a4b5c6d7
+Revises: a9f3c2d1e4b7
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/alembic/versions/f1a2b3c4d5e6_add_store_line_credentials.py` at line
4, The migration header's "Revises:" line must match the Alembic down_revision
value to avoid confusion; open the migration module
(f1a2b3c4d5e6_add_store_line_credentials.py), locate the module docstring
"Revises:" entry and the down_revision variable, and update the "Revises:" text
to exactly match the down_revision identifier (and ensure the top-level revision
value is correct if needed) so both identifiers are consistent.
backend/scripts/provision_stores.py-52-102 (1)

52-102: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Always dispose the engine, including early-failure paths.

Current early returns inside the loop skip cleanup at the end of the function, so engine disposal is not guaranteed.

💡 Suggested fix
     engine = create_async_engine(
         db_url,
         poolclass=NullPool,
         pool_pre_ping=True,
         connect_args=asyncpg_connect_args_for_url(db_url),
     )
     session_factory = async_sessionmaker(engine, expire_on_commit=False)
-
-    async with session_factory() as db:
-        for i, entry in enumerate(payload.stores, start=1):
-            try:
-                slug = await _resolve_slug(entry)
-            except ValueError as e:
-                print(f"[{i}] {entry.name}: ERROR — {e}", file=sys.stderr)
-                return 1
+    try:
+        async with session_factory() as db:
+            for i, entry in enumerate(payload.stores, start=1):
+                try:
+                    slug = await _resolve_slug(entry)
+                except ValueError as e:
+                    print(f"[{i}] {entry.name}: ERROR — {e}", file=sys.stderr)
+                    return 1
@@
-        if not dry_run:
-            await db.commit()
-            print("Done. Owners can log in with owner_email (Google) to claim the store.")
-
-    await engine.dispose()
+            if not dry_run:
+                await db.commit()
+                print("Done. Owners can log in with owner_email (Google) to claim the store.")
+    finally:
+        await engine.dispose()
     return 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/scripts/provision_stores.py` around lines 52 - 102, The engine
disposal is skipped by early returns inside the loop; wrap the main work so
engine.dispose() always runs (e.g., create a variable exit_code=0, move the
async with session_factory() block into a try/finally where the finally does
await engine.dispose(), and replace the in-loop return 1 occurrences with
setting exit_code=1 and breaking out of the loop), keeping the commit/rollback
logic around db and preserving calls to _resolve_slug and
upsert_store_from_provision; after the try/finally return exit_code.
backend/tests/test_store_provision.py-18-31 (1)

18-31: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the missing negative assertion for empty stores.

This test name says “requires stores” but only checks a valid payload. It should assert validation failure for {"stores": []}.

💡 Suggested fix
 import pytest
+from pydantic import ValidationError
@@
 def test_provision_file_requires_stores():
-    payload = StoreProvisionFile.model_validate(
-        {
-            "stores": [
-                {
-                    "name": "A",
-                    "owner_email": "a@b.com",
-                    "line_channel_access_token": "t",
-                    "line_channel_secret": "s",
-                }
-            ]
-        }
-    )
-    assert len(payload.stores) == 1
+    with pytest.raises(ValidationError):
+        StoreProvisionFile.model_validate({"stores": []})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_store_provision.py` around lines 18 - 31, Update
test_provision_file_requires_stores to include a negative assertion that an
empty "stores" list fails validation: call
StoreProvisionFile.model_validate({"stores": []}) inside a
pytest.raises(ValidationError) (import ValidationError from pydantic) and
optionally assert the raised error mentions "stores" or "must contain at least
one" to ensure the validator on StoreProvisionFile enforces non-empty stores.
backend/tests/test_multi_store_line.py-20-21 (1)

20-21: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert HTTP status/detail in missing-destination test.

The current assertion passes for any HTTPException. Please assert status_code == 400 (and ideally detail) to lock behavior.

Suggested fix
 def test_parse_webhook_destination_missing():
     from fastapi import HTTPException

-    with pytest.raises(HTTPException):
+    with pytest.raises(HTTPException) as exc_info:
         parse_webhook_destination("{}")
+    assert exc_info.value.status_code == 400
+    assert exc_info.value.detail == "Missing destination in webhook body"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_multi_store_line.py` around lines 20 - 21, Update the test
that calls parse_webhook_destination("{}") so it doesn't accept any
HTTPException: catch the exception and assert its status_code is 400 (and assert
the detail message matches the expected missing-destination text). Locate the
assertion around parse_webhook_destination in test_multi_store_line.py, capture
the raised HTTPException from pytest.raises and add assertions for
exc.value.status_code == 400 and exc.value.detail == "<expected detail string>"
(replace with the actual expected detail).
backend/app/services/user_service.py-36-37 (1)

36-37: ⚠️ Potential issue | 🟡 Minor

Align get_user_by_line_uid return annotation with repository None case

repo_get_user_by_line_uid() uses scalar_one_or_none() and is typed as Optional[Customer] (with User = Customer), and callers already handle if not user:—so the service signature should also return an optional type.

Suggested fix
-async def get_user_by_line_uid(db: AsyncSession, line_uid: str, store_id: int) -> User:
+async def get_user_by_line_uid(
+    db: AsyncSession, line_uid: str, store_id: int
+) -> Optional[User]:
     return await repo_get_user_by_line_uid(db, line_uid, store_id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/services/user_service.py` around lines 36 - 37, The service
function get_user_by_line_uid currently declares a non-optional User return but
calls repo_get_user_by_line_uid which returns Optional[Customer] (User =
Customer); update get_user_by_line_uid's return annotation to Optional[User] (or
Optional[Customer]) and import Optional from typing so the signature reflects
the repository's scalar_one_or_none() behavior and matches callers that check
"if not user:".
🧹 Nitpick comments (2)
frontend/package.json (1)

32-33: Align React 19 major with eslint-plugin-react-hooks major

frontend/package.json uses react/react-dom ^19.0.0 and frontend/package-lock.json resolves to react@19.2.6 / react-dom@19.2.6, while eslint-plugin-react-hooks resolves to 5.2.0 (^5.0.0). Bump eslint-plugin-react-hooks to v6+ to pick up the newer hook lint behavior (e.g., React.useEffect handling) and updated preset expectations (also applies to lines 46-47).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/package.json` around lines 32 - 33, Update the
eslint-plugin-react-hooks dependency in frontend/package.json to a v6+ range to
align with React 19 (react and react-dom are ^19.0.0); specifically change the
"eslint-plugin-react-hooks" version to "^6.0.0" (and likewise update any
duplicate entries on the other dependency lines referenced) and then run npm
install to regenerate frontend/package-lock.json so the lockfile resolves the
plugin to v6+.
backend/tests/test_models_pr_changes.py (1)

274-283: ⚡ Quick win

Include owner_email in the required Store column assertions.

This PR relies on owner-based binding; the model includes owner_email, but this structural test doesn’t assert it.

Suggested fix
     for c in (
         "id",
         "name",
         "slug",
         "line_channel_access_token",
         "line_channel_secret",
         "timezone",
         "active",
+        "owner_email",
         "owner_auth_user_id",
     ):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_models_pr_changes.py` around lines 274 - 283, The test
iterating over required Store columns is missing the "owner_email" field; update
the tuple in backend/tests/test_models_pr_changes.py (the for c in ( ... )
block) to include "owner_email" alongside "owner_auth_user_id" so the structural
assertion covers the owner_email column used by the model.

Comment on lines +102 to +103
status_code=status.HTTP_503_FORBIDDEN,
detail="Store is inactive",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
from fastapi import status
print("HTTP_403_FORBIDDEN:", hasattr(status, "HTTP_403_FORBIDDEN"))
print("HTTP_503_FORBIDDEN:", hasattr(status, "HTTP_503_FORBIDDEN"))
PY

Repository: chennjustin/Flower_Order_Bot

Length of output: 192


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the reported code region
sed -n '80,140p' backend/app/repositories/store_repository.py

# See how `status` is imported/defined in this file
rg -n "from fastapi import status|import status|HTTP_503_FORBIDDEN|HTTP_403_FORBIDDEN" backend/app/repositories/store_repository.py

# Check whether `HTTP_503_FORBIDDEN` appears anywhere else in the repo
rg -n "HTTP_503_FORBIDDEN" .

Repository: chennjustin/Flower_Order_Bot

Length of output: 2249


🌐 Web query:

FastAPI status constant HTTP_503_FORBIDDEN exists?

💡 Result:

No, the constant HTTP_503_FORBIDDEN does not exist in FastAPI [1][2]. FastAPI provides HTTP status code constants through its status module (which re-exports them from Starlette) [3][4]. The correct constant for a 503 status code is HTTP_503_SERVICE_UNAVAILABLE [1][5]. Similarly, the 403 status code is represented by the constant HTTP_403_FORBIDDEN [1][5]. You cannot combine these labels, as they refer to distinct HTTP status codes (503 for "Service Unavailable" and 403 for "Forbidden") [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check imports + context around the status usage
sed -n '1,80p' backend/app/repositories/store_repository.py
sed -n '90,120p' backend/app/repositories/store_repository.py

# Confirm the constant is only used here (and capture exact import of `status`)
rg -n "HTTP_503_FORBIDDEN|HTTP_403_FORBIDDEN|HTTP_503_SERVICE_UNAVAILABLE|HTTP_403_FORBIDDEN|from fastapi import status|import status" backend/app/repositories/store_repository.py

# Repo-wide occurrences (small)
rg -n "HTTP_503_FORBIDDEN" .

Repository: chennjustin/Flower_Order_Bot

Length of output: 4155


Fix invalid FastAPI/Starlette status constant for inactive stores

File: backend/app/repositories/store_repository.py (lines 102-103)

The branch for inactive stores uses status.HTTP_503_FORBIDDEN, which is not a valid FastAPI/Starlette status constant and will raise at runtime when reached.

                status_code=status.HTTP_503_FORBIDDEN,
                detail="Store is inactive",
Suggested fix
-                status_code=status.HTTP_503_FORBIDDEN,
+                status_code=status.HTTP_403_FORBIDDEN,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/repositories/store_repository.py` around lines 102 - 103, Replace
the invalid FastAPI/Starlette constant status.HTTP_503_FORBIDDEN in the "Store
is inactive" branch inside backend/app/repositories/store_repository.py with the
correct constant status.HTTP_503_SERVICE_UNAVAILABLE (or use
status.HTTP_403_FORBIDDEN if you intend a permission denial) so the raised
HTTPException uses a valid status code; locate the branch checking for inactive
stores (where the message "Store is inactive" is used) and update the
status_code to status.HTTP_503_SERVICE_UNAVAILABLE.

@DDTTYYSS DDTTYYSS closed this Jun 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants