# Pirkumu plāns — Functional Specification

**Status:** Approved v1.2 · 2026-07-14
**Scope of this document:** User-visible behavior for building and refreshing a multi-store purchase plan. Technical design and implementation are deliberately out of scope and follow after approval.

---

## 1. Purpose

`Pirkumu plāns` lets a user collect specific offers found across multiple part-number searches and turn them into an actionable list of orders to place at the stores themselves.

The comparator does not sell products or provide checkout. It helps the user answer:

- Which exact offers have I chosen?
- Which separate store orders do I need to place?
- Which selected parts are the same physical part offered by different stores?
- Which offers let me add another part to a store order I already plan to place?
- What is each store subtotal and the total merchandise cost?
- Have the saved prices or availability changed since I last checked them?

## 2. Terminology

| Term | Meaning |
|---|---|
| **Pirkumu plāns** | The user's saved collection of parts to buy and candidate store offers. |
| **Offer** | One store-specific result: store, brand, part number, price, availability, and outbound product link. |
| **Part identity** | The existing comparator identity for one physical variant: canonicalized brand + canonicalized part number. |
| **Part requirement** | One part identity the user plans to buy, with a desired quantity and one or more candidate offers. |
| **Candidate offer** | A saved store offer under a part requirement. One candidate is chosen; the others remain available for consideration. |
| **Chosen offer** | The candidate currently used to build store orders and calculate totals. Each part requirement has exactly one chosen offer. |
| **Store order** | All chosen offers from one store, shown as one group. It is a plan only; no order has been placed by the comparator. |
| **Offer snapshot** | The price and availability captured when an offer was added or last successfully refreshed. |

## 3. Entry Points

- The app header provides a persistent `Pirkumu plāns` action that opens a slide-in side panel over the current search.
- The header action shows the number of part requirements when the plan is not empty.
- Every store-specific offer row provides an `Add to purchase plan` action.
- A chosen offer and an offer saved as an alternative have distinct visible states and both provide a direct way to open the plan.
- Search results visibly identify offers from stores that already have chosen offers or saved alternatives in the plan.
- On desktop, the plan opens as a right-side panel with the current search left visible behind it.
- On mobile, the same panel becomes a full-screen view so order lines, alternatives, totals, and actions remain usable without a cramped narrow sheet.
- Closing the panel returns the user to the same search and scroll position.

Final Latvian UI wording will be kept in the app's centralized translation strings.

## 4. Adding Offers

### 4.1 New part

Adding an offer for a part identity not yet in the plan creates a part requirement with quantity `1`. The exact store offer becomes its first candidate and is chosen by default. The app does not add the cheapest offer automatically and does not add every offer in the product variant.

The saved item includes enough information to remain useful after the search results disappear:

- store identity and display name;
- brand and part number;
- product title and image, when present;
- desired quantity, stored on the part requirement and shared by its candidates;
- current unit price;
- structured and display availability;
- delivery note, when present;
- outbound product URL;
- time the offer was added;
- time and outcome of its latest freshness check.

### 4.2 Exact offer already present

Adding the exact same store offer again does not create another candidate or silently increase quantity. The app reports whether it is the chosen offer or a saved alternative and lets the user open the plan.

### 4.3 Same part from another store

When the plan already contains the same part identity from another store, adding the new offer opens a confirmation dialog. The dialog shows the current chosen offer and the new candidate, including store, price, and availability, and provides three explicit actions:

1. **Use new offer** — add the new candidate and make it the chosen offer. The previous candidate is retained as an alternative and the part quantity is preserved.
2. **Add as alternative** — retain the current choice and save the new candidate for consideration.
3. **Cancel** — leave the plan unchanged.

The dialog never requires deleting the previous candidate. Alternatives exist so the user can consider the complete multi-part order: a slightly more expensive individual offer may produce a better consolidated store order overall.

### 4.4 Store-consolidation cues in search results

When a store already has at least one candidate in the purchase plan, every result row from that store receives a visible consolidation cue. This helps the user identify both current consolidation opportunities and stores being considered as alternatives.

The cue has two strengths:

- **Store in current orders** — the store has at least one chosen offer and is already part of the proposed orders.
- **Store under consideration** — the store has saved alternatives but no chosen offers, so it is not yet part of the proposed orders.

The cue follows these rules:

- it appears as soon as the first chosen offer or alternative from that store is added to the plan;
- it appears in later searches and after page reloads because it derives from the persisted plan;
- it includes a text label or familiar icon with an accessible name, not color alone;
- it separately shows how many chosen offer lines and saved alternatives exist at that store;
- it is visually noticeable but weaker than the states for an exact chosen offer or saved alternative;
- the exact offer is clearly marked as `Chosen in purchase plan` or `Saved alternative`, not merely as a store match;
- it disappears from that store's search-result rows when the store no longer has any chosen offers or alternatives in the plan;
- it does not automatically add the offer or imply that the stores share a checkout;
- it does not change the existing cheapest-first ordering of offers.

Five result-row states must remain distinguishable:

| State | Meaning |
|---|---|
| **No plan relationship** | The offer's store has no candidates in the plan. |
| **Store under consideration** | This store has saved alternatives but is not part of the current proposed orders. |
| **Store in current orders** | Another chosen offer from this store makes consolidation possible. |
| **Saved alternative** | This exact offer is retained for consideration but does not contribute to orders or totals. |
| **Chosen offer** | This exact offer currently contributes to a store order and the plan totals. |

## 5. Purchase Plan Panel

The slide-in panel provides two views of the same saved data:

1. **Orders** — the primary view, grouped by store and containing only chosen offers.
2. **Parts and alternatives** — grouped by part requirement, showing every candidate and allowing the chosen offer to be changed.

Switching views does not duplicate data or create separate plans.

### 5.1 Orders by store

Chosen offers are grouped by store. Each store group shows:

- store name;
- number of chosen lines and total item quantity;
- merchandise subtotal for that store;
- all chosen items, their quantities, unit prices, line totals, and availability;
- a direct `View at store` action for each item.

The user can select any of these store-group orderings:

- alphabetical by store name;
- highest merchandise subtotal first;
- most chosen offer lines first.

The default is most chosen offer lines first. The selected ordering persists with the plan. Ties are resolved alphabetically so the list remains stable.

### 5.2 Parts and alternatives

Each part requirement shows:

- brand, part number, title, and matching-part marker;
- desired quantity;
- all saved candidate offers with store, unit price, availability, and latest check time;
- which candidate is currently chosen;
- the candidate's price difference from the current choice;
- the projected number of store orders, chosen lines at that candidate's store, and store subtotal if that candidate were chosen;
- whether choosing the candidate would add a new store order, remove an otherwise empty store order, or consolidate the part into an existing one.

The impact preview uses merchandise prices only and does not claim which option is best, because shipping and checkout adjustments are unknown.

Choosing an alternative is a single deliberate action. It atomically makes that candidate the chosen offer, retains the former choice as an alternative, and immediately recalculates store groups, counts, subtotals, and the grand total.

### 5.3 Totals

The plan summary shows:

- number of separate store orders required;
- number of unique part identities;
- number of chosen offer lines;
- number of saved alternatives, shown separately and not treated as purchases;
- total quantity;
- merchandise grand total across all store groups.

All line totals, store subtotals, and the grand total are calculated from chosen offers only as `latest known unit price × quantity`. Alternative candidates never contribute to order counts, quantities, subtotals, or the grand total.

The total is labelled **Preču kopsumma** rather than an order total. Shipping, store discounts, minimum-order fees, payment fees, deposits, and other checkout adjustments are not known and are excluded. If one or more chosen offers could not be freshly verified, the summary clearly marks the total as not fully verified. An unverified alternative still appears in the freshness warning but does not make the chosen-offer total unverified.

### 5.4 Matching-part markers

Because the order view is grouped by store while alternatives are organized by part, related information can appear far apart. The plan gives every unique part identity a stable visual marker:

- all occurrences of the same part use the same accent color;
- different parts cycle through a varied, visually distinct palette (the requested "rainbow" treatment);
- color is used on a restrained marker, border, or label rather than as a full-row background;
- the marker also includes text, such as the brand + part number and a repeated-part indicator, so color is never the only way to recognize a match;
- the chosen order line and every candidate shown in the parts-and-alternatives view carry the same marker;
- the chosen order line states how many saved alternatives exist for that part and links to them.

The marker assignment is deterministic from the part identity, so it remains stable while items are added, removed, refreshed, regrouped, or restored after a page reload.

## 6. Editing the Plan

The user can:

- increase or decrease a part requirement's quantity, with a minimum of `1`;
- enter a quantity directly;
- remove an alternative candidate;
- change which candidate is chosen;
- remove a complete part requirement;
- clear the complete plan after confirmation.

Removing a chosen candidate when alternatives remain requires the user to choose a replacement or remove the complete part requirement. It must not silently choose another store. Removing the final chosen item in a store group removes that empty group. All affected counts and totals update immediately.

## 7. Persistence

- The backend stores one shared purchase plan indefinitely. User identification is intentionally omitted while the application has a single-user deployment model.
- The plan survives searches, page reloads, browser restarts, and application restarts.
- A user can add offers on one device and continue with the same plan from another device connected to the same app backend.
- The browser keeps a local fallback copy for immediate startup, offline failure recovery, and migration from versions that stored the plan only in local storage.
- When the backend has no plan, an existing valid browser plan is uploaded automatically. When the backend already has a plan, the backend version is authoritative.
- Opening the plan checks the backend for changes made on another device. Unsaved local changes are never replaced by a background reload.
- Invalid or obsolete stored entries must not break the app. Valid entries remain usable, and unusable entries are ignored with a clear recovery message.
- If backend loading or saving fails, the app clearly warns that the browser copy may differ from the shared plan and retries on a later change or reload.

## 8. Refreshing Prices and Availability

Freshness checking is required functionality, not a best-effort display enhancement.

### 8.1 User actions

- Opening the plan does not automatically contact stores.
- When any candidate snapshot is more than 15 minutes old, lacks an observation time, or has an unresolved refresh outcome, the plan shows a prominent `Offers may be outdated` banner.
- The banner shows the relevant age or unresolved count and provides a `Refresh prices and availability` action for the complete plan.
- A refresh action also remains available when the warning banner is not shown, so the user can request a fresh check at any time.
- A failed or unresolved item can be retried individually.
- Refreshing is incremental: each item or store group updates as its store responds. One slow, failed, or challenge-gated store does not block results from other stores.
- The warning clears only when every saved candidate has a recent successful check. Partial refresh success replaces it with a precise warning about the candidates that remain unverified.
- The app never implies that opening the plan alone refreshed anything.

### 8.2 Direct product refresh requirement

- A plan refresh checks every chosen and alternative candidate through its saved product page or a product-specific store API used by that page.
- The refresh must target the exact saved product. It must not silently repeat the store's broad part-number search.
- A direct product refresh does not use the comparator's ordinary 15-minute search-result cache.
- Requests for the same exact store product within one refresh operation are deduplicated.
- Existing politeness limits, timeouts, and store circuit breakers continue to apply.
- A store adapter may fetch the visible product page or call a stable product-specific price/availability endpoint exposed by that page. It must not use a general search response as a substitute.
- Freshness is claimed only when the exact product identity and its current price and availability are verified in a newly successful direct response.
- A store error, timeout, identity mismatch, unsupported product page, or unresolved human challenge is not presented as a successful freshness check.

### 8.3 Verifying the refreshed product

The direct response is verified against the saved offer using its store, product URL or store-specific product reference, canonicalized brand, and canonicalized part number.

The system must not substitute an analogue, a different brand, a different part number, or another search result. A redirect or page whose identity cannot be confirmed leaves the saved candidate unchanged and reports that the exact offer could not be verified.

### 8.4 Refresh outcomes

Every saved candidate receives one of these outcomes independently:

| Outcome | User-visible behavior |
|---|---|
| **Checking** | Progress is shown for that item or store. |
| **Up to date** | Price and availability matched the previous snapshot; the successful check time is updated. |
| **Price changed** | Previous and new unit prices are shown, direction and amount are clear, and totals use the new price. |
| **Availability changed** | Previous and new availability are shown. Price may remain unchanged. |
| **Price and availability changed** | Both changes are shown together. |
| **Offer no longer found** | The product page or product-specific endpoint explicitly reports that the product is gone or discontinued. Ordinary out-of-stock status remains an availability state, not `not found`. The item remains in the plan with its last known data, is excluded from any claim that the plan is fully verified, and offers retry/removal actions. |
| **Could not check store** | The old snapshot remains unchanged; the reason is shown when known and retry is available. |
| **Human check required** | The existing challenge-solving flow is offered; after completion, only affected refresh requests are retried. |

An explicit product-level `not found`, discontinued response, or equivalent confirmed state is different from an out-of-stock product and from a failed or unparseable store request. Only the confirmed gone/discontinued state can produce `Offer no longer found`.

### 8.5 Change reporting

- After refresh, a summary reports how many candidates were unchanged, changed, not found, or could not be checked.
- Changed chosen offers are highlighted in their store groups; changed alternatives are highlighted in the parts-and-alternatives view.
- The previous and current values remain visible until the user marks the changes reviewed.
- Change state is persisted with the plan, so reloading the page or continuing on another device does not erase an unseen warning.
- Every item shows when its current snapshot was last successfully checked.
- A later successful refresh compares against the most recently accepted snapshot, not the original price from the day the item was first added.

## 9. Store Links and Ordering

- The app continues to link to each product page through its existing outbound route.
- The comparator does not add products to a store's real cart, open multiple links automatically, place orders, or collect payment.
- The user completes each purchase at the corresponding store and confirms the final price, availability, compatibility, shipping, and fees there.

## 10. Empty and Error States

- An empty plan explains that offers can be added from search results and provides a clear return to search.
- If backend persistence is unavailable, the user is told that changes currently exist only in the browser fallback; searching and normal outbound links continue working.
- If the server is unavailable, the stored plan remains reviewable and editable. Refresh actions report the connectivity failure without deleting saved data.
- Refreshing never removes a saved item automatically because a store failed, returned no match, or changed its product URL.

## 11. Explicit Non-goals

The first version does not include:

1. Checkout or payment in the comparator.
2. Automatic insertion into stores' carts.
3. Shipping-cost calculation or a guaranteed final payable total.
4. User accounts, access control, and multiple users.
5. Sharing or exporting a plan.
6. Multiple named plans, projects, or vehicles.
7. Scheduled background refreshes, notifications, or price alerts.
8. Long-term price history beyond the previous value needed to explain the latest unreviewed change.

## 12. Acceptance Criteria

The feature is functionally complete when:

1. A user can add exact offers from several searches without losing earlier selections.
2. The plan survives application and browser restarts and can be continued from another device connected to the same backend.
3. Each part requirement has exactly one chosen offer and may retain additional candidates as alternatives.
4. Adding another store's offer for the same part allows either choosing it immediately or saving it as an alternative without deleting the previous candidate.
5. Only chosen offers contribute to store orders, quantities, subtotals, and the merchandise grand total.
6. Switching the chosen candidate immediately updates affected store orders and totals while retaining the former choice as an alternative.
7. The order view groups chosen offers by store, while the parts-and-alternatives view supports candidate comparison and selection.
8. Every alternative previews its price and store-consolidation impact before the user chooses it.
9. Matching-part information is linked across views by both a stable color marker and text.
10. Search results distinguish chosen offers, saved alternatives, stores in current orders, and stores only under consideration.
11. Adding or removing the first or final candidate for a store immediately updates that store's consolidation cues in the current results.
12. Consolidation cues do not change cheapest-first result ordering.
13. Store groups can be sorted alphabetically, by highest subtotal, or by most chosen lines, and the preference persists.
14. Removing parts and changing quantities immediately updates every affected count and total.
15. A stale-offer banner appears without automatically contacting stores and provides the full refresh action.
16. A full refresh attempts a direct product-page or product-specific API request for every chosen and alternative candidate, without repeating a broad part-number search, while deduplicating requests for the same exact product.
17. A successful refresh detects price and availability changes and updates totals when a chosen offer changed.
18. Failed, challenged, and no-longer-found candidates remain distinct states and never silently delete plan data.
19. The UI never describes stale, cached, failed, or unverified data as freshly checked.

## 13. Resolved Decisions

1. **Same part from several stores:** saved offers are alternatives under one part requirement. Exactly one is chosen for proposed orders and totals.
2. **Adding another candidate:** the user can make the new candidate the choice or save it as an alternative; the previous candidate is retained.
3. **Refresh trigger:** opening the plan does not contact stores. A freshness banner provides an explicit refresh action when snapshots are old or unresolved.
4. **Store ordering:** all three proposed orderings are selectable. The default is most chosen offer lines first, with alphabetical tie-breaking, and the preference persists locally.
5. **Plan presentation:** the plan is a right-side slide-in panel on desktop and a full-screen panel on mobile, preserving the underlying search state.
6. **Refresh source:** refresh checks the saved product page or its product-specific API and does not silently rerun the store's broad part-number search.
7. **Challenge-gated product page:** retain the last snapshot as unverified, offer the existing human solve flow, and retry only that exact product after solving. Do not fall back to broad search.
