Commit ab0ce9c
committed
feat(recommendations): editable per-row Term/Payment in single-bucket purchase modal (issue #111 sub-option iii)
Implements the per-row counterpart of issue #111's user-approved
design ("default override pre-populated at purchase time, allowing
user to select other options if desired"). Sub-option (ii) — per-bucket
Payment in the fan-out modal — landed in the prior commit. This
commit handles the OTHER purchase entry point: the single-bucket
`openPurchaseModal`, which opens when the bulk-purchase selection
collapses to one (provider, service, term) bucket.
# What changes
- `openPurchaseModal` now renders editable Term and Payment dropdowns
per row. Defaults walk the precedence:
1. Override: `rec.cloud_account_id` has a saved
`AccountServiceOverride` matching `(provider, service)` whose
`payment` is supported by `(provider, service, term)` →
seed from override; row's source-note span renders
"(from account override)".
2. Rec's own payment (the API stamps it at collection time):
seed from `rec.payment` if non-empty AND supported.
3. Defensive fallback: `paymentOptionsFor(provider, service,
term)[0]`. Reachable only from malformed test fixtures or
pre-#111 cached responses where the rec lacks a payment.
- Edits mutate `currentPurchaseRecommendations[idx]` in place;
`getPurchaseModalRecommendations()` returns the user's choices.
`app.ts::handleExecutePurchase` now reads `r.payment` per rec
(with a defensive `?? 'all-upfront'` for direct test-harness
callers that bypass the modal). This replaces the historical
hardcoded `'all-upfront'` on the single-bucket path that silently
dropped the toolbar's Payment for every single-bucket purchase
since the bulk-purchase toolbar shipped — a pre-existing bug
surfaced and fixed by this commit.
- Term changes (1yr ↔ 3yr) rebuild only that row's Payment
`<select>` options; if the prior Payment is no longer supported
for the new term, the first valid option wins and live state is
mirrored. The modal does NOT re-render mid-edit so other rows'
in-progress edits are preserved.
# Implementation notes
- `openPurchaseModal` is now `async`. It pre-fetches
`listAccountServiceOverrides(id)` once per distinct non-empty
`cloud_account_id` in the input set, in parallel via `Promise.all`,
and caches the responses in a per-call `Map`. Errors are swallowed
— the rec-payment / paymentOptionsFor[0] fallback always works,
so a transient API blip shouldn't block a purchase. Same pattern
as `openFanOutModal` (ii).
- The DOM build switches from a template-literal `innerHTML`
rewrite to `createElement` construction so the per-row controls
can carry live event listeners. All cell text is `textContent`
assignment — no HTML interpolation, no XSS surface.
- `LocalRecommendation` gains an optional `payment?: string` field
in `frontend/src/types.ts`. The runtime data already carries it
(the API `Recommendation` defines `payment: string`), so this is
a type-additive change. No mapping changes required.
- The override-fetch+cache pattern is duplicated across
`openFanOutModal` (ii) and `openPurchaseModal` (iii). Documented
in code; follow-up issue will consolidate them into a shared
`frontend/src/lib/overrides.ts` helper once both surfaces have
shipped (avoiding scope creep on this PR).
# Out of scope (deliberately, with follow-up issues)
- **Recommendations stay unfiltered.** The recommendations page itself
is unchanged — overrides are applied at purchase time, not at
listing time.
- **`enabled=false`, `coverage`, and include/exclude lists are still
decorative.** Only `payment` is consumed (at purchase time, by
this PR + ii). For those other override fields to do anything,
`ListStoredRecommendations` / `RecommendationFilter` need to
become account-aware (option B). Filed as a follow-up.
- **Multi-account fan-out buckets still fall back to toolbar.**
Same scope discipline as (ii). Filed as a follow-up.
# Tests
5 new tests in `frontend/src/__tests__/recommendations.test.ts`
inside the `'Issue #111 (iii): per-row Payment seed in
openPurchaseModal'` describe block:
(a) Single rec, override matches and has supported payment →
live state + select value + source-note all reflect override.
(b) Single rec, no matching override → seed from rec.payment;
no source-note.
(c) Single rec, override has unsupported payment for the
`(provider, service, term)` cell (AWS RDS 3yr no-upfront,
blocked by `cmd/validators.go:warnRDS3YearNoUpfront`) →
override ignored; rec.payment wins.
(d) User changes Term 1→3 → rec.term updates; row's Payment
options rebuilt to the 3yr-supported set; live state
consistent with the dropdown.
(e) User changes Payment dropdown → live state reflects the new
value (which `app.ts::handleExecutePurchase` reads verbatim).
Existing `openPurchaseModal` tests (4) updated to `await` the now-
async function and read `.textContent` instead of `.innerHTML`
(content unchanged, just the rendering mode).
All 1371 frontend tests pass (1366 baseline including (ii)'s 4
tests + 5 new). Three clean verification passes (jest + tsc +
webpack build) before commit. Go smoke test on `internal/api/...`
clean.
Closes #111 in combination with the prior (ii) commit.1 parent cbe11b6 commit ab0ce9c
4 files changed
Lines changed: 483 additions & 48 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
391 | 391 | | |
392 | 392 | | |
393 | 393 | | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
394 | 397 | | |
395 | 398 | | |
396 | 399 | | |
| |||
633 | 636 | | |
634 | 637 | | |
635 | 638 | | |
636 | | - | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
637 | 643 | | |
638 | 644 | | |
639 | 645 | | |
| |||
646 | 652 | | |
647 | 653 | | |
648 | 654 | | |
649 | | - | |
| 655 | + | |
650 | 656 | | |
651 | 657 | | |
652 | 658 | | |
653 | 659 | | |
654 | 660 | | |
655 | | - | |
| 661 | + | |
656 | 662 | | |
657 | 663 | | |
658 | 664 | | |
659 | 665 | | |
660 | 666 | | |
661 | | - | |
| 667 | + | |
662 | 668 | | |
663 | 669 | | |
664 | | - | |
665 | | - | |
| 670 | + | |
| 671 | + | |
666 | 672 | | |
667 | 673 | | |
668 | | - | |
| 674 | + | |
669 | 675 | | |
670 | 676 | | |
671 | 677 | | |
672 | 678 | | |
673 | | - | |
| 679 | + | |
674 | 680 | | |
675 | 681 | | |
676 | | - | |
677 | | - | |
678 | | - | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
679 | 685 | | |
680 | 686 | | |
681 | | - | |
682 | | - | |
| 687 | + | |
| 688 | + | |
683 | 689 | | |
684 | | - | |
| 690 | + | |
685 | 691 | | |
686 | 692 | | |
687 | 693 | | |
| |||
1525 | 1531 | | |
1526 | 1532 | | |
1527 | 1533 | | |
| 1534 | + | |
| 1535 | + | |
| 1536 | + | |
| 1537 | + | |
| 1538 | + | |
| 1539 | + | |
| 1540 | + | |
| 1541 | + | |
| 1542 | + | |
| 1543 | + | |
| 1544 | + | |
| 1545 | + | |
| 1546 | + | |
| 1547 | + | |
| 1548 | + | |
| 1549 | + | |
| 1550 | + | |
| 1551 | + | |
| 1552 | + | |
| 1553 | + | |
| 1554 | + | |
| 1555 | + | |
| 1556 | + | |
| 1557 | + | |
| 1558 | + | |
| 1559 | + | |
| 1560 | + | |
| 1561 | + | |
| 1562 | + | |
| 1563 | + | |
| 1564 | + | |
| 1565 | + | |
| 1566 | + | |
| 1567 | + | |
| 1568 | + | |
| 1569 | + | |
| 1570 | + | |
| 1571 | + | |
| 1572 | + | |
| 1573 | + | |
| 1574 | + | |
| 1575 | + | |
| 1576 | + | |
| 1577 | + | |
| 1578 | + | |
| 1579 | + | |
| 1580 | + | |
| 1581 | + | |
| 1582 | + | |
| 1583 | + | |
| 1584 | + | |
| 1585 | + | |
| 1586 | + | |
| 1587 | + | |
| 1588 | + | |
| 1589 | + | |
| 1590 | + | |
| 1591 | + | |
| 1592 | + | |
| 1593 | + | |
| 1594 | + | |
| 1595 | + | |
| 1596 | + | |
| 1597 | + | |
| 1598 | + | |
| 1599 | + | |
| 1600 | + | |
| 1601 | + | |
| 1602 | + | |
| 1603 | + | |
| 1604 | + | |
| 1605 | + | |
| 1606 | + | |
| 1607 | + | |
| 1608 | + | |
| 1609 | + | |
| 1610 | + | |
| 1611 | + | |
| 1612 | + | |
| 1613 | + | |
| 1614 | + | |
| 1615 | + | |
| 1616 | + | |
| 1617 | + | |
| 1618 | + | |
| 1619 | + | |
| 1620 | + | |
| 1621 | + | |
| 1622 | + | |
| 1623 | + | |
| 1624 | + | |
| 1625 | + | |
| 1626 | + | |
| 1627 | + | |
| 1628 | + | |
| 1629 | + | |
| 1630 | + | |
| 1631 | + | |
| 1632 | + | |
| 1633 | + | |
| 1634 | + | |
| 1635 | + | |
| 1636 | + | |
| 1637 | + | |
| 1638 | + | |
| 1639 | + | |
| 1640 | + | |
| 1641 | + | |
| 1642 | + | |
| 1643 | + | |
| 1644 | + | |
| 1645 | + | |
| 1646 | + | |
| 1647 | + | |
| 1648 | + | |
| 1649 | + | |
| 1650 | + | |
| 1651 | + | |
| 1652 | + | |
| 1653 | + | |
| 1654 | + | |
| 1655 | + | |
| 1656 | + | |
| 1657 | + | |
| 1658 | + | |
| 1659 | + | |
| 1660 | + | |
| 1661 | + | |
| 1662 | + | |
| 1663 | + | |
| 1664 | + | |
| 1665 | + | |
| 1666 | + | |
| 1667 | + | |
| 1668 | + | |
| 1669 | + | |
| 1670 | + | |
| 1671 | + | |
| 1672 | + | |
| 1673 | + | |
| 1674 | + | |
| 1675 | + | |
| 1676 | + | |
| 1677 | + | |
| 1678 | + | |
| 1679 | + | |
| 1680 | + | |
| 1681 | + | |
| 1682 | + | |
| 1683 | + | |
| 1684 | + | |
| 1685 | + | |
| 1686 | + | |
| 1687 | + | |
| 1688 | + | |
| 1689 | + | |
| 1690 | + | |
| 1691 | + | |
| 1692 | + | |
| 1693 | + | |
| 1694 | + | |
| 1695 | + | |
| 1696 | + | |
| 1697 | + | |
| 1698 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
286 | 286 | | |
287 | 287 | | |
288 | 288 | | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
289 | 295 | | |
290 | 296 | | |
291 | 297 | | |
| |||
294 | 300 | | |
295 | 301 | | |
296 | 302 | | |
297 | | - | |
| 303 | + | |
298 | 304 | | |
299 | 305 | | |
300 | 306 | | |
| |||
0 commit comments