@vtex/faststore-plugin-buyer-portal 2.0.7 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.github/workflows/release.yaml +4 -4
  2. package/AGENTS.md +6 -0
  3. package/CHANGELOG.md +30 -1
  4. package/docs/superpowers/plans/2026-06-26-contracts-listing-selection-search-pagination.md +1070 -0
  5. package/docs/superpowers/specs/2026-06-26-contracts-listing-selection-search-pagination-design.md +157 -0
  6. package/package.json +1 -1
  7. package/plugin.config.js +8 -0
  8. package/public/buyer-portal-icons.svg +37 -1
  9. package/specs/001-suma-phase-2/checklists/requirements.md +36 -0
  10. package/specs/001-suma-phase-2/contracts/contracts-api.md +187 -0
  11. package/specs/001-suma-phase-2/contracts/feature-flag.md +91 -0
  12. package/specs/001-suma-phase-2/data-model.md +119 -0
  13. package/specs/001-suma-phase-2/plan.md +123 -0
  14. package/specs/001-suma-phase-2/quickstart.md +156 -0
  15. package/specs/001-suma-phase-2/research.md +150 -0
  16. package/specs/001-suma-phase-2/spec.md +186 -0
  17. package/specs/001-suma-phase-2/tasks.md +253 -0
  18. package/specs/suma-add-contracts.md +224 -0
  19. package/specs/suma-contract-listing.md +295 -0
  20. package/specs/suma-contract-switching.md +201 -0
  21. package/specs/suma-remove-contracts.md +233 -0
  22. package/specs/suma-set-default-contract.md +208 -0
  23. package/src/features/budgets/layouts/BudgetsLayout/BudgetsLayout.tsx +113 -105
  24. package/src/features/buying-policies/components/BasicBuyingPolicyDrawer/BasicBuyingPolicyDrawer.tsx +2 -2
  25. package/src/features/buying-policies/layouts/BuyingPoliciesLayout/BuyingPoliciesLayout.tsx +60 -46
  26. package/src/features/contracts/clients/ContractsClient.ts +45 -0
  27. package/src/features/contracts/components/AddContractsDrawer/AddContractsDrawer.tsx +323 -0
  28. package/src/features/contracts/components/AddContractsDrawer/add-contracts-drawer.scss +282 -0
  29. package/src/features/contracts/components/ConfirmRemoveDrawer/ConfirmRemoveDrawer.tsx +58 -0
  30. package/src/features/contracts/components/ConfirmRemoveDrawer/confirm-remove-drawer.scss +18 -0
  31. package/src/features/contracts/components/ContractListItem/ContractListItem.tsx +120 -0
  32. package/src/features/contracts/components/ContractSelectAllRow/ContractSelectAllRow.tsx +37 -0
  33. package/src/features/contracts/components/ContractSelectAllRow/index.ts +2 -0
  34. package/src/features/contracts/components/ContractSelectionBar/ContractSelectionBar.tsx +47 -0
  35. package/src/features/contracts/components/ContractSelectionBar/index.ts +2 -0
  36. package/src/features/contracts/components/ContractSelectionList/ContractSelectionList.tsx +55 -0
  37. package/src/features/contracts/components/ContractSwitchOverlay/ContractSwitchOverlay.tsx +36 -0
  38. package/src/features/contracts/components/ContractSwitchOverlay/contract-switch-overlay.scss +7 -0
  39. package/src/features/contracts/components/ContractsListHeader/ContractsListHeader.tsx +58 -0
  40. package/src/features/contracts/components/ContractsListHeader/index.ts +2 -0
  41. package/src/features/contracts/components/ErrorRemoveDrawer/ErrorRemoveDrawer.tsx +69 -0
  42. package/src/features/contracts/components/UnableToRemoveContractsDrawer/UnableToRemoveContractsDrawer.tsx +52 -0
  43. package/src/features/contracts/components/UnableToRemoveContractsDrawer/unable-to-remove-contracts-drawer.scss +29 -0
  44. package/src/features/contracts/components/index.ts +14 -0
  45. package/src/features/contracts/hooks/__tests__/contractListingHelpers.test.ts +71 -0
  46. package/src/features/contracts/hooks/__tests__/contractSwitchChannelHelpers.test.ts +32 -0
  47. package/src/features/contracts/hooks/__tests__/useAddContracts.test.ts +63 -0
  48. package/src/features/contracts/hooks/__tests__/useContractSelection.test.ts +72 -0
  49. package/src/features/contracts/hooks/__tests__/useRemoveContracts.test.ts +130 -0
  50. package/src/features/contracts/hooks/__tests__/useSetDefaultContract.test.ts +82 -0
  51. package/src/features/contracts/hooks/contractListingHelpers.ts +45 -0
  52. package/src/features/contracts/hooks/contractSelectionHelpers.ts +28 -0
  53. package/src/features/contracts/hooks/contractSwitchChannelHelpers.ts +9 -0
  54. package/src/features/contracts/hooks/index.ts +19 -0
  55. package/src/features/contracts/hooks/useAddContracts.ts +27 -0
  56. package/src/features/contracts/hooks/useContractListing.ts +71 -0
  57. package/src/features/contracts/hooks/useContractSelection.ts +53 -0
  58. package/src/features/contracts/hooks/useContractSwitchChannel.ts +49 -0
  59. package/src/features/contracts/hooks/useListAvailableContracts.ts +27 -0
  60. package/src/features/contracts/hooks/useRemoveContracts.ts +21 -0
  61. package/src/features/contracts/hooks/useSetDefaultContract.ts +21 -0
  62. package/src/features/contracts/hooks/useSwitchContract.ts +76 -0
  63. package/src/features/contracts/layouts/ContractInformationLayout/ContractInformationLayout.tsx +316 -0
  64. package/src/features/contracts/layouts/ContractListingLayout/ContractListingLayout.tsx +415 -0
  65. package/src/features/contracts/layouts/ContractListingLayout/ContractSettingsNav.tsx +83 -0
  66. package/src/features/contracts/layouts/ContractListingLayout/contract-listing-layout.scss +468 -0
  67. package/src/features/contracts/layouts/ContractListingLayout/utils.ts +4 -0
  68. package/src/features/contracts/layouts/index.ts +8 -0
  69. package/src/features/contracts/services/__tests__/contracts-client.test.ts +148 -0
  70. package/src/features/contracts/services/__tests__/list-attached-contracts.service.test.ts +95 -0
  71. package/src/features/contracts/services/add-contracts.service.ts +27 -0
  72. package/src/features/contracts/services/index.ts +24 -0
  73. package/src/features/contracts/services/list-attached-contracts.service.ts +40 -0
  74. package/src/features/contracts/services/list-available-contracts.service.ts +32 -0
  75. package/src/features/contracts/services/remove-contract.service.ts +27 -0
  76. package/src/features/contracts/services/remove-contracts.service.ts +36 -0
  77. package/src/features/contracts/services/set-default-contract.service.ts +27 -0
  78. package/src/features/contracts/types/ContractData.ts +1 -0
  79. package/src/features/contracts/utils/__tests__/changeContractToken.test.ts +141 -0
  80. package/src/features/contracts/utils/changeContractToken.ts +122 -0
  81. package/src/features/contracts/utils/clearPersistedSessionState.ts +74 -0
  82. package/src/features/org-units/components/OrgUnitDetailsNavbar/OrgUnitDetailsNavbar.tsx +45 -3
  83. package/src/features/org-units/components/OrgUnitDetailsNavbar/org-unit-details-navbar.scss +35 -0
  84. package/src/features/org-units/layouts/OrgUnitsLayout/OrgUnitsLayout.tsx +54 -48
  85. package/src/features/roles/layout/RolesLayout/RolesLayout.tsx +32 -23
  86. package/src/features/shared/components/BuyerPortalProvider/BuyerPortalProvider.tsx +1 -0
  87. package/src/features/shared/components/HeaderInside/HeaderInside.tsx +8 -1
  88. package/src/features/shared/components/HeaderInside/header-inside.scss +9 -0
  89. package/src/features/shared/layouts/BaseTabsLayout/SidebarMenu.tsx +6 -97
  90. package/src/features/shared/layouts/ContractTabsLayout/ContractTabsLayout.tsx +42 -5
  91. package/src/features/shared/layouts/LoadingTabsLayout/LoadingTabsLayout.tsx +1 -1
  92. package/src/features/shared/layouts/SumaPageLayout/FullSidebarNav.tsx +124 -0
  93. package/src/features/shared/layouts/SumaPageLayout/SumaPageLayout.tsx +56 -0
  94. package/src/features/shared/layouts/SumaPageLayout/SumaSidebar.tsx +80 -0
  95. package/src/features/shared/layouts/SumaPageLayout/SumaSidebarDrawer.tsx +48 -0
  96. package/src/features/shared/layouts/SumaPageLayout/index.ts +6 -0
  97. package/src/features/shared/layouts/SumaPageLayout/suma-sidebar-drawer.scss +37 -0
  98. package/src/features/shared/layouts/index.ts +6 -0
  99. package/src/features/shared/services/feature-flags/__tests__/get-feature-flags.service.test.ts +71 -0
  100. package/src/features/shared/services/feature-flags/get-feature-flags.service.ts +1 -0
  101. package/src/features/shared/utils/buyerPortalRoutes.ts +9 -0
  102. package/src/features/shared/utils/constants.ts +1 -1
  103. package/src/features/shared/utils/getSumaListingSidebarLinks.ts +18 -0
  104. package/src/features/shared/utils/index.ts +1 -0
  105. package/src/features/users/layouts/UsersLayout/UsersLayout.tsx +97 -103
  106. package/src/pages/contract-information.tsx +128 -0
  107. package/src/pages/contracts.tsx +104 -0
  108. package/src/pages/home.tsx +10 -6
  109. package/src/pages/org-unit-details.tsx +17 -13
  110. package/src/themes/layouts.scss +2 -0
  111. package/.agents/skills/create-page/SKILL.md +0 -154
  112. package/.agents/skills/create-page/metadata.json +0 -14
  113. package/.specify/memory/constitution.md +0 -83
@@ -0,0 +1,295 @@
1
+ # Feature Specification: SUMA Phase 2 — Contract Listing Page
2
+
3
+ **Feature Branch**: `feat/suma-listing`
4
+
5
+ **Created**: 2026-06-17
6
+
7
+ **Status**: Done
8
+
9
+ **Parent spec**: `specs/001-suma-phase-2/spec.md` (User Stories 1 & 2)
10
+
11
+ **Depends on**: `feat/suma-infra` — feature flag, route registration, `listAttachedContractsService`
12
+
13
+ ---
14
+
15
+ ## Business Context
16
+
17
+ ### Problem
18
+
19
+ When the `suma` feature flag is enabled, the account homepage (`org-unit-details`) must be replaced by a dedicated Contract Listing page. Currently there is no such page. Without it, users with multiple contracts have no way to view, distinguish, or select their contracts under the SUMA Phase 2 flow.
20
+
21
+ The listing page is the entry point of the new flow: it shows all contracts available to the user's Organizational Unit, highlights the default contract, and supports multi-contract selection via keyboard shortcuts to enable future bulk admin actions (add/remove — implemented in a later task).
22
+
23
+ ### Goals
24
+
25
+ - Replace `org-unit-details` as the account homepage when `featureFlags.suma === true`.
26
+ - Display all contracts for the Organizational Unit with clear visual hierarchy.
27
+ - Highlight the default contract (`isDefault === true`) with a visual badge.
28
+ - Support multi-select with keyboard shortcuts (Cmd/Ctrl+A, Cmd/Ctrl+Click, Shift+Click).
29
+ - Handle the single-contract edge case: hide removal actions when only one contract exists.
30
+
31
+ ### Out of Scope
32
+
33
+ - Contract switching (Profile page as Contract Information) — separate task (`specs/suma-contract-switching.md`).
34
+
35
+ ---
36
+
37
+ ## User Scenarios & Testing
38
+
39
+ ### User Story 1 — View Contract Listing as Homepage (Priority: P1)
40
+
41
+ When the `suma` flag is on, a buyer navigating to the account homepage lands on the Contract Listing page instead of `org-unit-details`.
42
+
43
+ **Acceptance Scenarios**:
44
+
45
+ 1. **Given** the `suma` feature flag is `true` and the user opens the account homepage, **When** the page loads, **Then** the Contract Listing page is displayed showing all contracts for the user's Organizational Unit.
46
+ 2. **Given** the `suma` feature flag is `false`, **When** the user opens the account homepage, **Then** the original `org-unit-details` page is shown with no regression.
47
+ 3. **Given** the loader is fetching contracts, **When** the data is pending, **Then** a loading skeleton is displayed.
48
+ 4. **Given** the loader fails, **When** the page renders, **Then** the existing `ErrorTabsLayout` is shown.
49
+
50
+ ---
51
+
52
+ ### User Story 2 — Default Contract Highlighted (Priority: P1)
53
+
54
+ The contract with `isDefault === true` is visually distinguished from other contracts.
55
+
56
+ **Acceptance Scenarios**:
57
+
58
+ 1. **Given** a contract has `isDefault: true`, **When** the listing renders, **Then** a "Default" badge is displayed on that contract's row.
59
+ 2. **Given** no contract has `isDefault: true`, **When** the listing renders, **Then** no badge is shown and all rows look equal.
60
+
61
+ ---
62
+
63
+ ### User Story 3 — Multi-Select with Keyboard Shortcuts (Priority: P2)
64
+
65
+ A buyer on the Contract Listing page can select multiple contracts using keyboard shortcuts.
66
+
67
+ **Acceptance Scenarios**:
68
+
69
+ 1. **Given** a buyer is on the Contract Listing page, **When** they press Cmd+A (Mac) or Ctrl+A (Windows/Linux), **Then** all contracts are selected without navigating anywhere.
70
+ 2. **Given** a buyer is on the Contract Listing page, **When** they Cmd+Click / Ctrl+Click a contract, **Then** that contract is toggled in the selection without navigating to its detail.
71
+ 3. **Given** a buyer has selected one contract and Shift+Clicks another, **Then** all contracts between the two are added to the selection as a range.
72
+ 4. **Given** one or more contracts are selected, **When** the buyer clicks a contract without a modifier key, **Then** the selection is cleared and the click proceeds as normal.
73
+ 5. **Given** contracts are selected, **When** the selection count is > 0, **Then** a bulk action toolbar appears showing the count and available actions.
74
+
75
+ ---
76
+
77
+ ### User Story 4 — Single-Contract State (Priority: P1)
78
+
79
+ When the Organizational Unit has exactly one contract, removal actions are hidden.
80
+
81
+ **Acceptance Scenarios**:
82
+
83
+ 1. **Given** an Organizational Unit has exactly 1 contract, **When** the listing renders, **Then** no "Remove" button or bulk remove action is displayed.
84
+ 2. **Given** an Organizational Unit has 2+ contracts, **When** contracts are selected, **Then** the bulk toolbar includes a "Remove" action.
85
+
86
+ ---
87
+
88
+ ### Key Scenarios
89
+
90
+ | Scenario | Pre-condition | Steps | Expected result |
91
+ |----------|--------------|-------|----------------|
92
+ | **Happy path** | Flag on, unit has 3 contracts, 1 is default | Load page | All 3 contracts listed; default has badge; no selection |
93
+ | **Single contract** | Flag on, unit has 1 contract | Load page | 1 contract shown; no remove action visible |
94
+ | **Cmd+A select all** | Flag on, 3 contracts | Press Cmd+A | All 3 highlighted; bulk toolbar shows count=3 |
95
+ | **Shift+Click range** | Flag on, 3 contracts | Click row 1, Shift+Click row 3 | Rows 1–3 highlighted |
96
+ | **Flag off** | Flag `suma=false` | Load account homepage | `org-unit-details` renders normally |
97
+ | **Load error** | Service throws | Page render | `ErrorTabsLayout` shown |
98
+ | **Empty list** | Unit has 0 contracts | Load page | Empty state shown |
99
+
100
+ ---
101
+
102
+ ## Requirements
103
+
104
+ ### Functional
105
+
106
+ - **FR-001**: Display all contracts from `listAttachedContractsService` for the current `orgUnitId`.
107
+ - **FR-002**: Show a "Default" badge on the contract where `isDefault === true`.
108
+ - **FR-003**: Show a loading skeleton while the loader is pending.
109
+ - **FR-004**: Show `ErrorTabsLayout` when the loader fails.
110
+ - **FR-005**: Show an empty state when the contract list is empty.
111
+ - **FR-006**: Support Cmd+A / Ctrl+A to select all contracts (no navigation).
112
+ - **FR-007**: Support Cmd+Click / Ctrl+Click to toggle individual contract selection (no navigation).
113
+ - **FR-008**: Support Shift+Click to select a contiguous range.
114
+ - **FR-009**: Show a bulk action toolbar (contract count) when `selectedIds.size > 0`.
115
+ - **FR-010**: Include a "Remove" action in the bulk toolbar only when `contracts.length > 1`.
116
+ - **FR-011**: Hide the "Remove" action entirely when `contracts.length === 1`.
117
+ - **FR-012**: Preserve existing `org-unit-details` behavior when `featureFlags.suma` is `false`.
118
+
119
+ ### Non-functional
120
+
121
+ - Page must pass `make lint` and `make typecheck` (zero new errors).
122
+ - All new hooks and pure functions must have unit tests.
123
+
124
+ ---
125
+
126
+ ## Arch Decisions
127
+
128
+ ### Decision 1 — Data source
129
+
130
+ **Use `listAttachedContractsService`** (from `feat/suma-infra`, calls `GET units/{orgUnitId}/contracts/attached?details=true`) rather than the existing `getContractsByOrgUnitIdService`.
131
+
132
+ **Rationale**: The confirmed BFF v1.0.2 endpoint for SUMA is `/contracts/attached`. The old endpoint (`/contracts`) is used by the legacy flow and should not be conflated with the new one.
133
+
134
+ **Fallback**: If `listAttachedContractsService` returns null (error), render `ErrorTabsLayout`.
135
+
136
+ ---
137
+
138
+ ### Decision 2 — Selection state in a hook
139
+
140
+ **`useContractSelection`** manages `selectedIds: Set<string>` as local React state. It exposes `isSelected(id)`, `handleSelectAll(allIds)`, `handleModifierClick(id)`, `handleShiftClick(id, allIds)`, `clearSelection()`.
141
+
142
+ **Rationale**: Self-contained hook keeps the layout lean and allows unit testing without rendering. No global/shared state needed — selection is local to the listing page session.
143
+
144
+ **Cross-platform**: Uses `event.metaKey || event.ctrlKey` to detect Cmd/Ctrl on Mac and Windows/Linux.
145
+
146
+ ---
147
+
148
+ ### Decision 3 — Page structure mirrors `org-unit-details`
149
+
150
+ The new `contracts.tsx` page follows the exact same loader pattern as `org-unit-details.tsx`:
151
+ - `withLoaderErrorBoundary` wrapping a `withAuthLoader` callback
152
+ - Fetches `orgUnit`, `contracts`, and `user` in parallel
153
+ - Renders a layout component; passes error to `ErrorTabsLayout`
154
+
155
+ **Rationale**: Consistency with existing page structure. Reduces cognitive overhead for the team. Reuses `OrgUnitDetailsNavbar` and `OrgUnitBreadcrumb` for navigation continuity.
156
+
157
+ ---
158
+
159
+ ### Decision 4 — Single-contract gate at layout level
160
+
161
+ The `isSingleContract` flag (`contracts.length <= 1`) is computed in `ContractListingLayout` and passed as a prop to `ContractSelectionList` and (later) to remove-action components. This mirrors how `OrgUnitsDetailsLayout` handles the same condition today.
162
+
163
+ ---
164
+
165
+ ## Technical Contract
166
+
167
+ ### New Files
168
+
169
+ ```
170
+ src/
171
+ ├── pages/
172
+ │ └── contracts.tsx # NEW page
173
+
174
+ └── features/contracts/
175
+ ├── hooks/
176
+ │ └── useContractSelection.ts # NEW hook
177
+
178
+ ├── components/
179
+ │ ├── ContractListItem/
180
+ │ │ └── ContractListItem.tsx # NEW component
181
+ │ └── ContractSelectionList/
182
+ │ └── ContractSelectionList.tsx # NEW component
183
+
184
+ └── layouts/
185
+ └── ContractListingLayout/
186
+ └── ContractListingLayout.tsx # NEW layout
187
+ ```
188
+
189
+ ---
190
+
191
+ ### `contracts.tsx` — Page
192
+
193
+ ```typescript
194
+ // Loader data shape
195
+ type ContractsPageData = {
196
+ data: {
197
+ orgUnit: OrgUnitBasicData;
198
+ contracts: ContractData[]; // from listAttachedContractsService
199
+ user: UserData | null;
200
+ };
201
+ context: { clientContext: ClientContext };
202
+ hasError?: boolean;
203
+ error?: ErrorBoundaryProps;
204
+ };
205
+
206
+ // Loader: parallel fetch of orgUnit, contracts, user
207
+ // Renders ContractListingLayout or ErrorTabsLayout
208
+ ```
209
+
210
+ Route: `/pvt/organization-account/contracts/[orgUnitId]` (registered in `plugin.config.js` via infra task).
211
+
212
+ ---
213
+
214
+ ### `useContractSelection` — Hook
215
+
216
+ ```typescript
217
+ type UseContractSelectionReturn = {
218
+ selectedIds: Set<string>;
219
+ isSelected: (id: string) => boolean;
220
+ handleSelectAll: (allIds: string[]) => void;
221
+ handleModifierClick: (id: string) => void; // Cmd/Ctrl+Click
222
+ handleShiftClick: (id: string, allIds: string[]) => void;
223
+ clearSelection: () => void;
224
+ };
225
+ ```
226
+
227
+ Internal state: `selectedIds: Set<string>`, `lastSelectedIndex: number`.
228
+
229
+ ---
230
+
231
+ ### `ContractListItem` — Component
232
+
233
+ ```typescript
234
+ type ContractListItemProps = {
235
+ contract: ContractData; // includes isDefault?: boolean
236
+ isSelected: boolean;
237
+ onModifierClick: (id: string, event: React.MouseEvent) => void;
238
+ onShiftClick: (id: string, event: React.MouseEvent) => void;
239
+ };
240
+ ```
241
+
242
+ Renders: contract name (fallback to email), "Default" badge when `isDefault === true`, selection highlight when `isSelected === true`.
243
+
244
+ Click behaviour:
245
+ - Plain click → no-op (activation TBD)
246
+ - Cmd/Ctrl+Click → calls `onModifierClick`
247
+ - Shift+Click → calls `onShiftClick`
248
+
249
+ ---
250
+
251
+ ### `ContractSelectionList` — Component
252
+
253
+ ```typescript
254
+ type ContractSelectionListProps = {
255
+ contracts: ContractData[];
256
+ isSingleContract: boolean;
257
+ selectedIds: Set<string>;
258
+ onKeyDown: (event: React.KeyboardEvent) => void; // for Cmd+A
259
+ onModifierClick: (id: string, event: React.MouseEvent) => void;
260
+ onShiftClick: (id: string, event: React.MouseEvent) => void;
261
+ };
262
+ ```
263
+
264
+ Renders the full list of `ContractListItem` components. Attaches `onKeyDown` for Cmd+A detection. Shows bulk toolbar when `selectedIds.size > 0` — toolbar includes remove button gated by `!isSingleContract`.
265
+
266
+ ---
267
+
268
+ ### `ContractListingLayout` — Layout
269
+
270
+ ```typescript
271
+ type ContractListingLayoutProps = {
272
+ data: {
273
+ orgUnit: OrgUnitBasicData;
274
+ contracts: ContractData[];
275
+ user: UserData | null;
276
+ };
277
+ loading?: boolean;
278
+ };
279
+ ```
280
+
281
+ Structure:
282
+ 1. `GlobalLayout` wrapper
283
+ 2. `OrgUnitDetailsNavbar` (reused from org-units)
284
+ 3. `OrgUnitBreadcrumb` in `HeaderInside`
285
+ 4. `ContractSelectionList` with `useContractSelection` wired in
286
+
287
+ ---
288
+
289
+ ### Modified Files
290
+
291
+ | File | Change |
292
+ |------|--------|
293
+ | `src/features/contracts/components/index.ts` | Export `ContractListItem`, `ContractSelectionList` |
294
+ | `src/features/contracts/hooks/index.ts` | Export `useContractSelection` |
295
+ | `src/features/contracts/layouts/index.ts` | Export `ContractListingLayout` |
@@ -0,0 +1,201 @@
1
+ # Feature Specification: SUMA Phase 2 — Contract Switching & Profile (Contract Information)
2
+
3
+ **Feature Branch**: `feat/suma-switching`
4
+
5
+ **Created**: 2026-06-18
6
+
7
+ **Status**: Done
8
+
9
+ **Parent spec**: `specs/001-suma-phase-2/spec.md` (User Stories 3 & 4)
10
+
11
+ **Depends on**:
12
+ - `feat/suma-infra` — feature flag, routes, BFF service layer
13
+ - `feat/suma-listing` — Contract Listing page (provides the entry point for this flow)
14
+
15
+ > **Terminology**: In SUMA, **Contract Information** means the existing **Profile** page — route `/pvt/organization-account/profile/[orgUnitId]/[contractId]` (`buyerPortalRoutes.profileDetails`). After switching contracts, the buyer lands on Profile to operate under the selected contract. The separately registered `/contract-information` route is **not** the switch destination.
16
+
17
+ ---
18
+
19
+ ## Business Context
20
+
21
+ ### Problem
22
+
23
+ SUMA Phase 2 enables the same user to belong to multiple contracts (accounts). After the
24
+ Contract Listing page is in place, users need to switch into a specific contract's
25
+ context and manage it. Switching requires updating the session token via the BFF, then
26
+ landing on the Profile page (Contract Information) for that contract.
27
+
28
+ ### SUMA Mental Model
29
+
30
+ Switching contracts updates the **active commercial context** for the session. A plain
31
+ click on a contract row:
32
+
33
+ 1. Calls the BFF `switch-properties` endpoint (`changeContractToken`) with the target contract id.
34
+ 2. Clears persisted host session state so SSR re-runs under the new cookie.
35
+ 3. Full-navigates to the **Profile page** (`buyerPortalRoutes.profileDetails`) for that contract.
36
+ 4. Broadcasts to other tabs so open listing pages refresh.
37
+
38
+ ### Goals
39
+
40
+ - Enable click-through from the listing to the Profile page (Contract Information) for each contract.
41
+ - Show a loading overlay while the BFF switch and navigation are in progress.
42
+ - Surface an error toast on the listing when the switch fails (user stays on listing).
43
+ - Synchronize open tabs via `BroadcastChannel` so the listing refreshes when the user
44
+ switches or removes a contract in another tab.
45
+
46
+ ### Out of Scope
47
+
48
+ - The `/contract-information` dedicated route as a switch destination (Profile is canonical).
49
+ - Propagate-to-children on remove (deferred).
50
+
51
+ ---
52
+
53
+ ## User Scenarios & Testing
54
+
55
+ ### User Story 1 — Switch to Profile (Contract Information) (Priority: P1)
56
+
57
+ A buyer clicks a contract row in the listing to enter that contract's context.
58
+
59
+ **Acceptance Scenarios**:
60
+
61
+ 1. **Given** the Contract Listing page is displayed, **When** the user clicks a contract row (no modifier key), **Then** a loading overlay appears, the BFF switch-properties call runs, and on success the browser navigates to the Profile page (`/profile/[orgUnitId]/[contractId]`) for that contract.
62
+ 2. **Given** the BFF switch fails, **When** the request errors, **Then** the user remains on the Contract Listing and an error toast is shown.
63
+ 3. **Given** the user clicks with Cmd/Ctrl, **Then** the multi-select toggle fires (no navigation) — existing listing behaviour is unchanged.
64
+ 4. **Given** the switch succeeds, **When** the BroadcastChannel message is sent, **Then** all other open tabs showing the listing reload.
65
+
66
+ ---
67
+
68
+ ### User Story 2 — Profile Page (Contract Information) (Priority: P1)
69
+
70
+ The Profile page shows the contract's identity, basic details, and navigation under the new session context.
71
+
72
+ **Acceptance Scenarios**:
73
+
74
+ 1. **Given** the user lands on `/pvt/organization-account/profile/[orgUnitId]/[contractId]` after a successful switch, **When** the page loads, **Then** the contract name and profile details are displayed under the selected contract context.
75
+ 2. **Given** the user is on a SUMA org page with the sidebar visible, **When** they use the "Back to contracts" link, **Then** they are navigated back to the Contract Listing page.
76
+ 3. **Given** the user clicks a contract-scoped tab (Addresses, Payment methods, etc.), **Then** the browser navigates to the corresponding existing buyer portal route for that contract.
77
+
78
+ ---
79
+
80
+ ### User Story 3 — Remove Contract (Priority: P1)
81
+
82
+ Contract removal is handled from the **Contract Listing** page (bulk toolbar and per-row dots menu). See `specs/suma-remove-contracts.md`.
83
+
84
+ ---
85
+
86
+ ### User Story 4 — Error Handling on Switch (Priority: P1)
87
+
88
+ When the BFF switch fails, the user stays on the listing with feedback.
89
+
90
+ **Acceptance Scenarios**:
91
+
92
+ 1. **Given** the BFF switch-properties call fails, **When** the error is caught, **Then** the loading overlay clears and an error toast appears on the Contract Listing page.
93
+ 2. **Given** the Contract Listing page mounts with `?error=contract-load-failed` (legacy redirect param), **Then** an auto-dismissing toast fires with the message "Failed to load contract information." and the query param is removed.
94
+
95
+ ---
96
+
97
+ ### User Story 5 — Tab Synchronisation (Priority: P2)
98
+
99
+ State changes in one tab propagate to other open tabs.
100
+
101
+ **Acceptance Scenarios**:
102
+
103
+ 1. **Given** the user is on the listing in Tab 1 and Tab 2 switches to a contract (Profile page), **Then** Tab 1 reloads to reflect the current state.
104
+ 2. **Given** the user removes a contract in Tab 2, **Then** Tab 1's listing refreshes and the removed contract no longer appears.
105
+ 3. **Given** the browser does not support `BroadcastChannel`, **Then** the app degrades gracefully — no crash, just no cross-tab sync.
106
+
107
+ ---
108
+
109
+ ### Key Scenarios
110
+
111
+ | Scenario | Pre-condition | Action | Expected result |
112
+ |----------|--------------|--------|----------------|
113
+ | **Happy path — switch** | Listing, 3 contracts | Click row | Loading overlay → BFF switch → Profile page loads |
114
+ | **Switch failure** | BFF returns error | Click row | Toast on listing; overlay clears; no navigation |
115
+ | **Tab sync — switch** | Listing open in Tab 1 | Tab 2 switches contract | Tab 1 reloads |
116
+ | **Tab sync — remove** | Listing open in Tab 1 | Tab 2 removes a contract | Tab 1 reloads; row gone |
117
+ | **Modifier click** | Listing page | Cmd+Click row | Multi-select toggled; no navigation |
118
+ | **Error toast not replayed** | Listing with `?error=…` | Reload page | No second toast |
119
+
120
+ ---
121
+
122
+ ## Requirements
123
+
124
+ ### Functional
125
+
126
+ - **FR-001**: Plain click on `ContractRow` calls `useSwitchContract` → BFF `changeContractToken` → navigates to `buyerPortalRoutes.profileDetails({ orgUnitId, contractId })`.
127
+ - **FR-002**: Cmd/Ctrl click and Shift+Click preserve existing multi-select behaviour (no switch).
128
+ - **FR-003**: On successful switch, fire `BroadcastChannel` message `{ type: 'contract-activated', contractId }`.
129
+ - **FR-004**: A loading overlay (`ContractSwitchOverlay`) MUST cover the row while the switch is in progress.
130
+ - **FR-005**: On switch failure, show an error toast on the listing; user MUST remain on the listing.
131
+ - **FR-006**: Profile page renders under the new session context after full navigation.
132
+ - **FR-007**: Listing reads `?error=contract-load-failed` on mount (legacy), shows toast, removes query param from URL.
133
+ - **FR-008**: `BroadcastChannel` listener in listing calls `router.reload()` on any incoming message.
134
+ - **FR-009**: `BroadcastChannel` usage is guarded by `typeof BroadcastChannel !== 'undefined'`.
135
+
136
+ ### Non-functional
137
+
138
+ - Zero new TypeScript errors (pre-existing baseline errors excluded).
139
+ - Page must pass `make lint`.
140
+ - New hooks and pure functions must have unit tests.
141
+
142
+ ---
143
+
144
+ ## Arch Decisions
145
+
146
+ ### Decision 1 — BFF switch + Profile navigation
147
+
148
+ **Chosen approach**: Plain click on `ContractRow` calls `useSwitchContract`, which:
149
+
150
+ 1. POSTs to BFF `switch-properties` via `changeContractToken(contractId)`.
151
+ 2. Clears persisted host session state (`clearPersistedSessionState`).
152
+ 3. Broadcasts `{ type: 'contract-activated', contractId }` to other tabs.
153
+ 4. Full-navigates to `buyerPortalRoutes.profileDetails({ orgUnitId, contractId })`.
154
+
155
+ A loading overlay covers the row until navigation completes. On failure, the overlay clears and an error toast is shown on the listing.
156
+
157
+ **Rationale**: Switching contracts requires updating the session cookie so SSR and store context reflect the selected commercial agreement. The Profile page is the canonical Contract Information destination in the buyer portal.
158
+
159
+ **Failure path**: Toast on listing; previous contract context remains active.
160
+
161
+ ---
162
+
163
+ ### Decision 2 — BroadcastChannel for tab sync
164
+
165
+ **Chosen approach**: `useContractSwitchChannel` wraps `BroadcastChannel` with channel name `'buyer-portal-active-contract'`. Listing subscribes and calls `router.reload()` on any message.
166
+
167
+ **Message shapes**:
168
+ ```typescript
169
+ type ChannelMessage = { type: 'contract-activated'; contractId: string };
170
+ ```
171
+
172
+ **Who sends**: `useSwitchContract` after a successful BFF switch.
173
+
174
+ **Who listens**: `ContractListingLayout`.
175
+
176
+ ---
177
+
178
+ ## Technical Contract
179
+
180
+ ### Key implementation files
181
+
182
+ ```
183
+ src/features/contracts/
184
+ ├── hooks/
185
+ │ ├── useSwitchContract.ts # BFF switch + profile navigation
186
+ │ └── useContractSwitchChannel.ts # BroadcastChannel wrapper
187
+ ├── components/
188
+ │ └── ContractSwitchOverlay/ # Loading overlay during switch
189
+ └── utils/
190
+ ├── changeContractToken.ts # BFF switch-properties call
191
+ └── clearPersistedSessionState.ts # Clear host session after switch
192
+
193
+ src/pages/profile.tsx # Contract Information destination
194
+ ```
195
+
196
+ ### Modified files
197
+
198
+ | File | Change |
199
+ |------|--------|
200
+ | `ContractListItem.tsx` | Plain click → `useSwitchContract`; loading overlay; error toast on failure |
201
+ | `ContractListingLayout.tsx` | Subscribe to BroadcastChannel → `router.reload()`; legacy `?error` toast handling |