Skip to content

feat: packing list for trips - #272

Open
TheBlackSide243 wants to merge 2 commits into
Anyesh:mainfrom
TheBlackSide243:pr/packing-list
Open

TheBlackSide243 wants to merge 2 commits into
Anyesh:mainfrom
TheBlackSide243:pr/packing-list

Conversation

@TheBlackSide243

@TheBlackSide243 TheBlackSide243 commented Oct 18, 2026 •

Copy link
Copy Markdown

Description

Adds a Packing page: pick a destination, the dates, the occasion and whether laundry is
available, and Wardrowbe builds a packing list from the user's own wardrobe, grouped by role
(tops, bottoms, outerwear, shoes...). It uses the daily forecast for the destination when the
trip starts within the 16-day forecast range and a seasonal estimate otherwise (the response says
which). Rain days ask for rain-friendly outerwear; roles the wardrobe cannot cover are flagged.

  • POST /api/v1/packing (destination is geocoded with the existing location search), no new
    tables (services/packing_service.py).
  • Frontend page /dashboard/packing, nav entry, translations for all 8 locales.

This is one of a set of small, independent PRs (each branches from main and can be reviewed or
dropped on its own); they only overlap in the nav/router registration lines.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • CI/CD or build changes

Checklist

  • I have read the CONTRIBUTING guide
  • My code follows the project's coding style
  • I have added tests that prove my fix/feature works
  • New and existing tests pass locally
  • I have updated documentation as needed
  • My changes don't introduce new warnings or errors

Testing

  • Backend: ruff check, ruff format --check, migration heads check and the full pytest suite on top of current main (2040 passed).
  • Frontend: npm run lint, i18n:check, vocab:check, tsc --noEmit and vitest (693 passed).
  • Running on my own self-hosted instance (Docker Compose on TrueNAS, Ollama).

Test Environment

  • Docker Compose
  • Kubernetes
  • Local development

🤖 Generated with Claude Code

Summary by Sourcery

Add weather-aware trip packing plans built from each user’s wardrobe.

New Features:

  • Add an authenticated packing-plan API that combines trip details, destination weather, occasion, laundry availability, and the user’s wardrobe into grouped packing recommendations.
  • Add a dashboard packing page where users can create plans, view forecast or seasonal conditions, track packed items, and identify missing wardrobe roles.

Enhancements:

  • Use wardrobe suitability, preferences, exclusions, cleanliness, weather, season, and formality to prioritize items and flag uncovered needs.
  • Support forecast-based planning within the available horizon with seasonal estimates and explicit uncertainty for trips beyond it.
  • Expose the packing page through desktop and mobile navigation with localized content across all supported locales.

Tests:

  • Add backend coverage for forecast handling, quantity planning, item selection, validation, authentication, and API behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Adds an end-to-end trip packing feature: an authenticated backend plans weather- and occasion-aware quantities from the user’s wardrobe, while a localized dashboard lets users generate, inspect, and mark items packed, with forecast fallback and uncovered-role reporting.

Sequence diagram for generating a trip packing plan

sequenceDiagram
    actor User
    participant PackingPage
    participant PackingAPI
    participant WeatherService
    participant PackingService
    participant Wardrobe

    User->>PackingPage: submit packing form
    PackingPage->>PackingAPI: POST /api/v1/packing
    PackingAPI->>WeatherService: geocode_location_name(destination)
    WeatherService-->>PackingAPI: coordinates and place
    opt trip starts within forecast horizon
        PackingAPI->>WeatherService: get_daily_forecast(latitude, longitude, span)
        WeatherService-->>PackingAPI: forecast
    end
    PackingAPI->>PackingService: plan(user, start, end, latitude, forecast, occasion, laundry)
    PackingService->>Wardrobe: wardrobe(user)
    Wardrobe-->>PackingService: ready wardrobe items
    PackingService->>PackingService: summarize_forecast(forecast, start, end, season)
    PackingService->>PackingService: plan_quantities(days, weather, laundry, preferences)
    PackingService->>PackingService: choose_items(items, plan, weather, season, occasion, preferences)
    PackingService-->>PackingAPI: weather, season, packing groups
    PackingAPI-->>PackingPage: PackingResponse
    PackingPage-->>User: grouped list, forecast or estimate, missing roles
Loading

Flow diagram for trip packing plan selection

flowchart TD
    A["Trip details: destination, dates, occasion, laundry"] --> B["Geocode destination"]
    B --> C{Trip within 16-day forecast horizon?}
    C -->|Yes| D["Use daily forecast"]
    C -->|No| E["Use seasonal estimate"]
    D --> F["Summarize temperatures and rain days"]
    E --> F
    F --> G["Calculate quantities by wardrobe role"]
    G --> H["Score wardrobe items by weather, season, and occasion"]
    H --> I["Build grouped packing list"]
    I --> J["Flag roles with insufficient suitable items"]
Loading

File-Level Changes

Change Details Files
Introduces an authenticated packing-plan API that validates trip inputs, geocodes destinations, uses near-term forecasts or seasonal estimates, and returns weather summaries plus wardrobe-backed packing groups and missing roles.
  • Added request validation for destination, occasion, date range, trip length, and past trips.
  • Integrated geocoding and weather services with explicit not-found, service-unavailable, and forecast-fallback behavior.
  • Calculated quantities based on trip length, laundry availability, weather, and user preferences.
  • Ranked eligible ready wardrobe items by weather, season, occasion, garment role, and favorites while excluding unsuitable items.
  • Registered the new endpoint and covered core service and API behavior with unit and integration tests.
backend/app/api/packing.py
backend/app/api/router.py
backend/app/services/packing_service.py
backend/tests/test_packing.py
Adds a dashboard packing workflow for entering trip details and interactively reviewing the generated list.
  • Added destination, date, occasion, and laundry controls with mutation/error/loading states.
  • Displayed forecast or estimated weather, season, rain information, grouped wardrobe items, missing roles, and pack/unpack toggles.
  • Added a packing hook with typed request and response models.
  • Registered the page in desktop and mobile navigation.
frontend/app/dashboard/packing/page.tsx
frontend/lib/hooks/use-packing.ts
frontend/components/sidebar.tsx
frontend/components/mobile-sidebar.tsx
Adds localized UI support for the packing feature across all supported locales.
  • Registered the packing translation namespace.
  • Added translated navigation labels and packing-page strings for eight locales.
frontend/lib/i18n/locales.ts
frontend/messages/de/nav.json
frontend/messages/de/packing.json
frontend/messages/en/nav.json
frontend/messages/en/packing.json
frontend/messages/fr/nav.json
frontend/messages/fr/packing.json
frontend/messages/it/nav.json
frontend/messages/it/packing.json
frontend/messages/ja/nav.json
frontend/messages/ja/packing.json
frontend/messages/ko/nav.json
frontend/messages/ko/packing.json
frontend/messages/zh-CN/nav.json
frontend/messages/zh-CN/packing.json
frontend/messages/zh-TW/nav.json
frontend/messages/zh-TW/packing.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions github-actions Bot added javascript Pull requests that update javascript code python Pull requests that update python code labels Oct 18, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 12 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="backend/app/services/packing_service.py" line_range="98" />
<code_context>
+        temp_min=min(d.temp_min for d in in_trip),
+        temp_max=max(d.temp_max for d in in_trip),
+        rain_days=sum(1 for d in in_trip if is_wet(d)),
+        estimated=False,
+        days=in_trip,
+    )
</code_context>
<issue_to_address>
**Partial forecasts appear complete**

When the trip overlaps available forecast data but includes dates outside it, `summarize_forecast` uses only covered days and sets `estimated=False`, so temperatures, rain counts, and packing decisions omit unforecast days while the response presents the summary as covering the whole trip.

Detect incomplete date coverage in `summarize_forecast` and mark the summary estimated rather than presenting partial data as a full-trip forecast.

Also at `backend/app/services/packing_service.py:90`, `backend/app/services/packing_service.py:97`, `backend/app/api/packing.py:96-99`, `backend/app/api/packing.py:116-120`.
</issue_to_address>

### Comment 2
<location path="backend/app/api/packing.py" line_range="103" />
<code_context>
+
+    weather_service = WeatherService()
+    try:
+        place = await weather_service.geocode_location_name(request.destination)
+    except GeocodingServiceError as e:
+        logger.error(f"Geocoding service error: {e}")
</code_context>
<issue_to_address>
**Geocoding bursts disrupt searches**

When users submit enough distinct uncached destinations to exceed Nominatim’s request limit, `plan_packing` sends uncached destinations to the external geocoder without a shared request limit, so Nominatim throttles requests and packing requests fail with 503s, disrupting destination searches for other users.

Add a shared request throttle or rate limit for geocoding requests.
</issue_to_address>

### Comment 3
<location path="backend/app/api/packing.py" line_range="120" />
<code_context>
+        span = min(FORECAST_HORIZON_DAYS, (request.end_date - today).days + 1)
+        try:
+            forecast = await weather_service.get_daily_forecast(latitude, longitude, max(1, span))
+        except WeatherServiceError as e:
+            # A seasonal estimate still gives a usable list; the response says it is estimated.
+            logger.warning(f"Forecast unavailable for packing: {e}")
</code_context>
<issue_to_address>
**Malformed forecasts cause server errors**

When the weather provider returns invalid JSON or a successful response with an unexpected shape, `get_daily_forecast` lets JSON decoding or response-shape errors escape as exceptions other than `WeatherServiceError`, so this handler does not use its seasonal fallback and the packing request returns a server error.

Validate and normalize the provider response in `get_daily_forecast`, converting decoding and shape failures to `WeatherServiceError` so packing can use its seasonal fallback.
</issue_to_address>

### Comment 4
<location path="backend/app/api/packing.py" line_range="119" />
<code_context>
+    if days_ahead < FORECAST_HORIZON_DAYS:
+        span = min(FORECAST_HORIZON_DAYS, (request.end_date - today).days + 1)
+        try:
+            forecast = await weather_service.get_daily_forecast(latitude, longitude, max(1, span))
+        except WeatherServiceError as e:
+            # A seasonal estimate still gives a usable list; the response says it is estimated.
</code_context>
<issue_to_address>
**Invalid geocodes cause server errors**

When the geocoding provider returns numeric coordinates outside valid latitude or longitude bounds, `get_daily_forecast` raises `ValueError` for out-of-range coordinates, but `plan_packing` catches only `WeatherServiceError`, so the request returns an unhandled server error instead of a controlled response.

Validate geocoded coordinates or handle the forecast service's `ValueError` and return a controlled API error.
</issue_to_address>

### Comment 5
<location path="backend/app/services/packing_service.py" line_range="217" />
<code_context>
+                and_(
+                    ClothingItem.user_id == user.id,
+                    ClothingItem.status == ItemStatus.ready,
+                    ClothingItem.is_archived.is_(False),
+                )
+            )
</code_context>
<issue_to_address>
**Dirty clothes appear packable**

When a ready, non-archived wardrobe item is marked as needing a wash, `PackingService.wardrobe` includes the item without checking `needs_wash`, so the packing groups recommend clothes that are currently dirty.

Exclude items marked as needing a wash when loading the wardrobe for packing.
</issue_to_address>

### Comment 6
<location path="backend/app/services/packing_service.py" line_range="120" />
<code_context>
+        "full_body": 1 if days >= 3 else 0,
+        "footwear": 2 if days > 3 or weather.rain_days > 0 else 1,
+        "mid_layer": 1 if cool_evenings else 0,
+        "outer_layer": 1 if (cool_evenings or weather.rain_days > 0) and not hot else 0,
+        "accessory": 2 if cold else 0,
+    }
</code_context>
<issue_to_address>
**Rainy trips omit outerwear**

When the trip is hot and has at least one rainy day, `plan_quantities` sets `outer_layer` to zero whenever `hot` is true, so `choose_items` skips that role and the packing list omits rain-friendly outerwear.

Allow rainy days to request an outer layer even when the trip is hot.
</issue_to_address>

### Comment 7
<location path="backend/app/services/packing_service.py" line_range="242" />
<code_context>
+        preferences = await self.preferences(user)
+        quantities = plan_quantities(trip_length(start, end), weather, laundry, preferences)
+        groups = choose_items(
+            await self.wardrobe(user), quantities, weather, season, occasion, preferences
+        )
+        return weather, season, groups
</code_context>
<issue_to_address>
**Excluded items get packed**

When a user has saved excluded item IDs that are also ready, non-archived wardrobe items, `PackingService.plan` passes wardrobe items to `choose_items` without filtering `preferences.excluded_item_ids`, so items the user excluded from recommendations can appear in the packing list.

Remove items in `preferences.excluded_item_ids` before choosing packing items.
</issue_to_address>

### Comment 8
<location path="backend/app/services/packing_service.py" line_range="44-51" />
<code_context>
+    "fall": (8.0, 17.0),
+}
+
+ROLE_ORDER = (
+    "outer_layer",
+    "mid_layer",
+    "base_top",
+    "full_body",
+    "bottom",
+    "footwear",
+    "accessory",
+)
+COLD_ACCESSORY_TYPES = {"scarf", "hat"}
</code_context>
<issue_to_address>
**Supported garments disappear**

When the wardrobe contains items assigned to a vocabulary role absent from `ROLE_ORDER`, `choose_items` skips roles absent from `ROLE_ORDER`, and `plan_quantities` does not request some roles such as `suit`; those garments never appear in packing results or missing-role warnings.

Include supported vocabulary roles in the packing role set and plan quantities for roles that need them.

Also at `backend/app/services/packing_service.py:188-189`.
</issue_to_address>

### Comment 9
<location path="backend/app/services/packing_service.py" line_range="171" />
<code_context>
+        elif weather.rain_days and item_type in RAIN_LAYER_TYPES:
+            score *= 1.15
+    if ITEM_ROLE.get(item_type) == "accessory" and item_type not in COLD_ACCESSORY_TYPES:
+        score *= 0.3
+    if item.favorite:
+        score *= 1.1
</code_context>
<issue_to_address>
**Belts can fill cold-accessory gaps**

When the trip is cold and the wardrobe has no suitable hat or scarf but does have a generic accessory that scores at least 0.67 before the penalty, `packing_score` only multiplies non-cold accessories by 0.3, and the resulting score can still meet `MIN_PACKING_SCORE`. In cold weather, a belt or bag can therefore be selected as one of the needed accessories, making the list appear covered without recommending a hat or scarf.

Exclude non-cold accessory types from cold-weather accessory candidates, or keep them from satisfying the cold-accessory requirement.

Also at `backend/app/services/packing_service.py:191`.
</issue_to_address>

### Comment 10
<location path="backend/app/services/packing_service.py" line_range="93" />
<code_context>
+    in_trip = [d for d in forecast if start.isoformat() <= d.date <= end.isoformat()]
+    if not in_trip:
+        low, high = SEASON_TEMPERATURES[season]
+        return TripWeather(temp_min=low, temp_max=high, rain_days=0, estimated=True)
+    return TripWeather(
+        temp_min=min(d.temp_min for d in in_trip),
</code_context>
<issue_to_address>
**Estimated trips report zero rain**

When the trip has no usable in-trip forecast, such as dates beyond the horizon or a forecast-service failure, `summarize_forecast` assigns `rain_days=0` when it has no forecast data, and the API presents that as the trip's rain-day count. For estimated trips this turns unknown precipitation into a dry forecast and can suppress rain gear from the packing list.

Represent precipitation as unknown for estimated weather or use an explicit conservative seasonal estimate instead of reporting zero rain days.
</issue_to_address>

### Comment 11
<location path="backend/tests/test_packing.py" line_range="101" />
<code_context>
+        weather = TripWeather(temp_min=15, temp_max=22, rain_days=2, estimated=False)
+        plan = plan_quantities(2, weather, laundry=False, preferences=None)
+        assert plan["outer_layer"] == 1
+        assert plan["footwear"] == 2
+
+
</code_context>
<issue_to_address>
**Rain layer assertion passes without rain logic**

When the outerwear plan relies only on `cool_evenings`, with no rain-based outerwear rule, `test_rain_adds_a_layer_and_spare_shoes` asserts `outer_layer == 1`, but at 15°C the default 10°C cold threshold makes `cool_evenings` true even without rain. Removing the rain-based outerwear rule therefore still passes this assertion, so the test does not verify that rain adds a layer.

Use a temperature where `cool_evenings` is false and `hot` is false, and compare rainy and dry plans to assert that rain changes the outer-layer count.
</issue_to_address>

### Comment 12
<location path="backend/tests/test_packing.py" line_range="208" />
<code_context>
+        assert groups["base_top"]["needed"] == 3
+        assert "base_top" in data["missing_roles"]
+        packed = [i for g in data["groups"] for i in g["items"]]
+        assert all(not i["is_archived"] for i in packed)
+        assert all(i["image_url"] for i in packed)
+
</code_context>
<issue_to_address>
**Archived-item check never exercises the coat**

When the trip does not request outerwear, as in this test’s dry 16°C forecast, `test_plans_from_the_wardrobe` adds an archived coat, but its dry 16°C forecast produces no outer-layer need. `choose_items` therefore never returns the coat, so even a wardrobe query that includes archived items passes the `all(not i["is_archived"] ...)` assertion.

Make the test require an outer layer and give the coat `status=ready` with `is_archived=True`, so an archived-item leak would cause the assertion to fail.
</issue_to_address>

Sourcery assessment

Approval pending. 12 findings to address first.

Blocking findings: backend/app/services/packing_service.py:98, backend/app/api/packing.py:103, backend/app/api/packing.py:120, backend/app/api/packing.py:119, backend/app/services/packing_service.py:217, and 7 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread backend/app/services/packing_service.py Outdated
temp_min=min(d.temp_min for d in in_trip),
temp_max=max(d.temp_max for d in in_trip),
rain_days=sum(1 for d in in_trip if is_wet(d)),
estimated=False,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High · Partial forecasts appear complete

When the trip overlaps available forecast data but includes dates outside it, summarize_forecast uses only covered days and sets estimated=False, so temperatures, rain counts, and packing decisions omit unforecast days while the response presents the summary as covering the whole trip.

Detect incomplete date coverage in summarize_forecast and mark the summary estimated rather than presenting partial data as a full-trip forecast.

Also at backend/app/services/packing_service.py:90, backend/app/services/packing_service.py:97, backend/app/api/packing.py:96-99, backend/app/api/packing.py:116-120.

Prompt for AI agents
In `backend/app/services/packing_service.py` at line 98:

**Partial forecasts appear complete**

When the trip overlaps available forecast data but includes dates outside it, `summarize_forecast` uses only covered days and sets `estimated=False`, so temperatures, rain counts, and packing decisions omit unforecast days while the response presents the summary as covering the whole trip.

Detect incomplete date coverage in `summarize_forecast` and mark the summary estimated rather than presenting partial data as a full-trip forecast.

Also at `backend/app/services/packing_service.py:90`, `backend/app/services/packing_service.py:97`, `backend/app/api/packing.py:96-99`, `backend/app/api/packing.py:116-120`.


weather_service = WeatherService()
try:
place = await weather_service.geocode_location_name(request.destination)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Geocoding bursts disrupt searches

When users submit enough distinct uncached destinations to exceed Nominatim’s request limit, plan_packing sends uncached destinations to the external geocoder without a shared request limit, so Nominatim throttles requests and packing requests fail with 503s, disrupting destination searches for other users.

Add a shared request throttle or rate limit for geocoding requests.

Prompt for AI agents
In `backend/app/api/packing.py` at line 103:

**Geocoding bursts disrupt searches**

When users submit enough distinct uncached destinations to exceed Nominatim’s request limit, `plan_packing` sends uncached destinations to the external geocoder without a shared request limit, so Nominatim throttles requests and packing requests fail with 503s, disrupting destination searches for other users.

Add a shared request throttle or rate limit for geocoding requests.

Comment thread backend/app/api/packing.py Outdated
span = min(FORECAST_HORIZON_DAYS, (request.end_date - today).days + 1)
try:
forecast = await weather_service.get_daily_forecast(latitude, longitude, max(1, span))
except WeatherServiceError as e:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Malformed forecasts cause server errors

When the weather provider returns invalid JSON or a successful response with an unexpected shape, get_daily_forecast lets JSON decoding or response-shape errors escape as exceptions other than WeatherServiceError, so this handler does not use its seasonal fallback and the packing request returns a server error.

Validate and normalize the provider response in get_daily_forecast, converting decoding and shape failures to WeatherServiceError so packing can use its seasonal fallback.

Prompt for AI agents
In `backend/app/api/packing.py` at line 120:

**Malformed forecasts cause server errors**

When the weather provider returns invalid JSON or a successful response with an unexpected shape, `get_daily_forecast` lets JSON decoding or response-shape errors escape as exceptions other than `WeatherServiceError`, so this handler does not use its seasonal fallback and the packing request returns a server error.

Validate and normalize the provider response in `get_daily_forecast`, converting decoding and shape failures to `WeatherServiceError` so packing can use its seasonal fallback.

if days_ahead < FORECAST_HORIZON_DAYS:
span = min(FORECAST_HORIZON_DAYS, (request.end_date - today).days + 1)
try:
forecast = await weather_service.get_daily_forecast(latitude, longitude, max(1, span))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Invalid geocodes cause server errors

When the geocoding provider returns numeric coordinates outside valid latitude or longitude bounds, get_daily_forecast raises ValueError for out-of-range coordinates, but plan_packing catches only WeatherServiceError, so the request returns an unhandled server error instead of a controlled response.

Validate geocoded coordinates or handle the forecast service's ValueError and return a controlled API error.

Prompt for AI agents
In `backend/app/api/packing.py` at line 119:

**Invalid geocodes cause server errors**

When the geocoding provider returns numeric coordinates outside valid latitude or longitude bounds, `get_daily_forecast` raises `ValueError` for out-of-range coordinates, but `plan_packing` catches only `WeatherServiceError`, so the request returns an unhandled server error instead of a controlled response.

Validate geocoded coordinates or handle the forecast service's `ValueError` and return a controlled API error.

and_(
ClothingItem.user_id == user.id,
ClothingItem.status == ItemStatus.ready,
ClothingItem.is_archived.is_(False),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Dirty clothes appear packable

When a ready, non-archived wardrobe item is marked as needing a wash, PackingService.wardrobe includes the item without checking needs_wash, so the packing groups recommend clothes that are currently dirty.

Exclude items marked as needing a wash when loading the wardrobe for packing.

Prompt for AI agents
In `backend/app/services/packing_service.py` at line 217:

**Dirty clothes appear packable**

When a ready, non-archived wardrobe item is marked as needing a wash, `PackingService.wardrobe` includes the item without checking `needs_wash`, so the packing groups recommend clothes that are currently dirty.

Exclude items marked as needing a wash when loading the wardrobe for packing.

Comment on lines +44 to +51
ROLE_ORDER = (
"outer_layer",
"mid_layer",
"base_top",
"full_body",
"bottom",
"footwear",
"accessory",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Supported garments disappear

When the wardrobe contains items assigned to a vocabulary role absent from ROLE_ORDER, choose_items skips roles absent from ROLE_ORDER, and plan_quantities does not request some roles such as suit; those garments never appear in packing results or missing-role warnings.

Include supported vocabulary roles in the packing role set and plan quantities for roles that need them.

Also at backend/app/services/packing_service.py:188-189.

Prompt for AI agents
In `backend/app/services/packing_service.py` at lines 44-51:

**Supported garments disappear**

When the wardrobe contains items assigned to a vocabulary role absent from `ROLE_ORDER`, `choose_items` skips roles absent from `ROLE_ORDER`, and `plan_quantities` does not request some roles such as `suit`; those garments never appear in packing results or missing-role warnings.

Include supported vocabulary roles in the packing role set and plan quantities for roles that need them.

Also at `backend/app/services/packing_service.py:188-189`.

Comment thread backend/app/services/packing_service.py Outdated
elif weather.rain_days and item_type in RAIN_LAYER_TYPES:
score *= 1.15
if ITEM_ROLE.get(item_type) == "accessory" and item_type not in COLD_ACCESSORY_TYPES:
score *= 0.3

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Belts can fill cold-accessory gaps

When the trip is cold and the wardrobe has no suitable hat or scarf but does have a generic accessory that scores at least 0.67 before the penalty, packing_score only multiplies non-cold accessories by 0.3, and the resulting score can still meet MIN_PACKING_SCORE. In cold weather, a belt or bag can therefore be selected as one of the needed accessories, making the list appear covered without recommending a hat or scarf.

Exclude non-cold accessory types from cold-weather accessory candidates, or keep them from satisfying the cold-accessory requirement.

Also at backend/app/services/packing_service.py:191.

Prompt for AI agents
In `backend/app/services/packing_service.py` at line 171:

**Belts can fill cold-accessory gaps**

When the trip is cold and the wardrobe has no suitable hat or scarf but does have a generic accessory that scores at least 0.67 before the penalty, `packing_score` only multiplies non-cold accessories by 0.3, and the resulting score can still meet `MIN_PACKING_SCORE`. In cold weather, a belt or bag can therefore be selected as one of the needed accessories, making the list appear covered without recommending a hat or scarf.

Exclude non-cold accessory types from cold-weather accessory candidates, or keep them from satisfying the cold-accessory requirement.

Also at `backend/app/services/packing_service.py:191`.

Comment thread backend/app/services/packing_service.py Outdated
in_trip = [d for d in forecast if start.isoformat() <= d.date <= end.isoformat()]
if not in_trip:
low, high = SEASON_TEMPERATURES[season]
return TripWeather(temp_min=low, temp_max=high, rain_days=0, estimated=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Estimated trips report zero rain

When the trip has no usable in-trip forecast, such as dates beyond the horizon or a forecast-service failure, summarize_forecast assigns rain_days=0 when it has no forecast data, and the API presents that as the trip's rain-day count. For estimated trips this turns unknown precipitation into a dry forecast and can suppress rain gear from the packing list.

Represent precipitation as unknown for estimated weather or use an explicit conservative seasonal estimate instead of reporting zero rain days.

Prompt for AI agents
In `backend/app/services/packing_service.py` at line 93:

**Estimated trips report zero rain**

When the trip has no usable in-trip forecast, such as dates beyond the horizon or a forecast-service failure, `summarize_forecast` assigns `rain_days=0` when it has no forecast data, and the API presents that as the trip's rain-day count. For estimated trips this turns unknown precipitation into a dry forecast and can suppress rain gear from the packing list.

Represent precipitation as unknown for estimated weather or use an explicit conservative seasonal estimate instead of reporting zero rain days.

Comment thread backend/tests/test_packing.py Outdated
weather = TripWeather(temp_min=15, temp_max=22, rain_days=2, estimated=False)
plan = plan_quantities(2, weather, laundry=False, preferences=None)
assert plan["outer_layer"] == 1
assert plan["footwear"] == 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Rain layer assertion passes without rain logic

When the outerwear plan relies only on cool_evenings, with no rain-based outerwear rule, test_rain_adds_a_layer_and_spare_shoes asserts outer_layer == 1, but at 15°C the default 10°C cold threshold makes cool_evenings true even without rain. Removing the rain-based outerwear rule therefore still passes this assertion, so the test does not verify that rain adds a layer.

Use a temperature where cool_evenings is false and hot is false, and compare rainy and dry plans to assert that rain changes the outer-layer count.

Prompt for AI agents
In `backend/tests/test_packing.py` at line 101:

**Rain layer assertion passes without rain logic**

When the outerwear plan relies only on `cool_evenings`, with no rain-based outerwear rule, `test_rain_adds_a_layer_and_spare_shoes` asserts `outer_layer == 1`, but at 15°C the default 10°C cold threshold makes `cool_evenings` true even without rain. Removing the rain-based outerwear rule therefore still passes this assertion, so the test does not verify that rain adds a layer.

Use a temperature where `cool_evenings` is false and `hot` is false, and compare rainy and dry plans to assert that rain changes the outer-layer count.

assert groups["base_top"]["needed"] == 3
assert "base_top" in data["missing_roles"]
packed = [i for g in data["groups"] for i in g["items"]]
assert all(not i["is_archived"] for i in packed)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Archived-item check never exercises the coat

When the trip does not request outerwear, as in this test’s dry 16°C forecast, test_plans_from_the_wardrobe adds an archived coat, but its dry 16°C forecast produces no outer-layer need. choose_items therefore never returns the coat, so even a wardrobe query that includes archived items passes the all(not i["is_archived"] ...) assertion.

Make the test require an outer layer and give the coat status=ready with is_archived=True, so an archived-item leak would cause the assertion to fail.

Prompt for AI agents
In `backend/tests/test_packing.py` at line 208:

**Archived-item check never exercises the coat**

When the trip does not request outerwear, as in this test’s dry 16°C forecast, `test_plans_from_the_wardrobe` adds an archived coat, but its dry 16°C forecast produces no outer-layer need. `choose_items` therefore never returns the coat, so even a wardrobe query that includes archived items passes the `all(not i["is_archived"] ...)` assertion.

Make the test require an outer layer and give the coat `status=ready` with `is_archived=True`, so an archived-item leak would cause the assertion to fail.

- A trip only partly covered by the forecast is an estimate: the seasonal
  range widens the extremes and rain is unknown (null) instead of zero.
- Unknown or forecast rain asks for a rain layer, even on a hot trip.
- Suits, legwear, socks and ties have a place; suit and tie on formal trips.
- Only hats and scarves fill the cold-weather accessory slots.
- Items excluded from recommendations stay out; ones needing a wash rank
  after clean ones.
- Malformed forecasts and out-of-range coordinates fall back to the season.
- Tighter tests for rain layers and archived items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update javascript code python Pull requests that update python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants