Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.workwell.measure;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.workwell.security.SecurityActor;
import java.sql.Array;
import java.sql.Timestamp;
import java.time.Instant;
Expand Down Expand Up @@ -197,16 +199,33 @@ public TerminologyMapping createTerminologyMapping(
String mappingStatus, Double mappingConfidence, String notes
) {
UUID id = UUID.randomUUID();
String resolvedStatus = mappingStatus != null ? mappingStatus : "PROPOSED";
jdbcTemplate.update("""
INSERT INTO terminology_mappings (id, local_code, local_display, local_system,
standard_code, standard_display, standard_system, mapping_status, mapping_confidence, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
id, localCode, localDisplay, localSystem,
standardCode, standardDisplay, standardSystem,
mappingStatus != null ? mappingStatus : "PROPOSED",
resolvedStatus,
mappingConfidence, notes
);
Map<String, Object> auditPayload = new HashMap<>();
auditPayload.put("mappingId", id.toString());
auditPayload.put("localCode", localCode);
auditPayload.put("localSystem", localSystem);
auditPayload.put("standardCode", standardCode);
auditPayload.put("standardSystem", standardSystem);
auditPayload.put("mappingStatus", resolvedStatus);
auditPayload.put("mappingConfidence", mappingConfidence);
jdbcTemplate.update(
"INSERT INTO audit_events (event_type, entity_type, entity_id, actor, payload_json) VALUES (?, ?, ?, ?, ?::jsonb)",
"TERMINOLOGY_MAPPING_CREATED",
"terminology_mapping",
id,
SecurityActor.currentActorOr("system"),
toJson(auditPayload)
);
return jdbcTemplate.queryForObject("""
SELECT id, local_code, local_display, local_system,
standard_code, standard_display, standard_system,
Expand All @@ -228,6 +247,14 @@ INSERT INTO terminology_mappings (id, local_code, local_display, local_system,
), id);
}

private String toJson(Map<String, Object> payload) {
try {
return objectMapper.writeValueAsString(payload);
} catch (JsonProcessingException ex) {
throw new IllegalStateException("Unable to serialize audit payload", ex);
}
}

// Private helpers

private void ensureDemoValueSets() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
import com.workwell.measure.ValueSetGovernanceService.TerminologyMapping;
import com.workwell.measure.ValueSetGovernanceService.ValueSetDiffResponse;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.test.context.support.WithMockUser;

@SpringBootTest
Expand All @@ -23,6 +25,9 @@ class ValueSetGovernanceIntegrationTest extends AbstractIntegrationTest {
@Autowired
private MeasureService measureService;

@Autowired
private JdbcTemplate jdbcTemplate;

@Test
@WithMockUser(username = "admin@workwell.dev", roles = "ADMIN")
void listTerminologyMappingsReturnsSeededData() {
Expand All @@ -49,6 +54,18 @@ void createTerminologyMappingPersists() {

List<TerminologyMapping> all = valueSetGovernanceService.listTerminologyMappings();
assertThat(all).anyMatch(m -> m.id().equals(created.id()));

// Audit event must be written for every terminology mapping creation (hard rule: every state change writes audit_event)
List<Map<String, Object>> auditRows = jdbcTemplate.queryForList(
"SELECT event_type, entity_type, entity_id, payload_json::text FROM audit_events WHERE entity_id = ?",
created.id());
assertThat(auditRows).hasSize(1);
Map<String, Object> audit = auditRows.get(0);
assertThat(audit.get("event_type")).isEqualTo("TERMINOLOGY_MAPPING_CREATED");
assertThat(audit.get("entity_type")).isEqualTo("terminology_mapping");
String payload = (String) audit.get("payload_json");
assertThat(payload).contains("LOCAL-TEST-001");
assertThat(payload).contains("PROPOSED");
}

@Test
Expand Down
50 changes: 50 additions & 0 deletions backend/src/test/java/com/workwell/web/AdminControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,56 @@ void listTerminologyMappingsReturnsOk() throws Exception {
.andExpect(jsonPath("$[0].mappingStatus").value("APPROVED"));
}

@Test
void createsTerminologyMappingWithProposedStatus() throws Exception {
UUID mappingId = UUID.fromString("dddddddd-dddd-4ddd-8ddd-dddddddddddd");
when(valueSetGovernanceService.createTerminologyMapping(
"ANNUAL-FIT-TEST", "Annual respirator fit test", "urn:workwell:demo",
"415070008", "Fitting of mask (procedure)", "http://snomed.info/sct",
"PROPOSED", 0.90, "Demo fit test mapping"
)).thenReturn(new ValueSetGovernanceService.TerminologyMapping(
mappingId,
"ANNUAL-FIT-TEST", "Annual respirator fit test", "urn:workwell:demo",
"415070008", "Fitting of mask (procedure)", "http://snomed.info/sct",
"PROPOSED", 0.90, null, null, "Demo fit test mapping"
));

mockMvc.perform(post("/api/admin/terminology-mappings")
.contentType("application/json")
.content("""
{
"localCode": "ANNUAL-FIT-TEST",
"localDisplay": "Annual respirator fit test",
"localSystem": "urn:workwell:demo",
"standardCode": "415070008",
"standardDisplay": "Fitting of mask (procedure)",
"standardSystem": "http://snomed.info/sct",
"mappingStatus": "PROPOSED",
"mappingConfidence": 0.90,
"notes": "Demo fit test mapping"
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.localCode").value("ANNUAL-FIT-TEST"))
.andExpect(jsonPath("$.standardCode").value("415070008"))
.andExpect(jsonPath("$.mappingStatus").value("PROPOSED"));
}

@Test
void rejectsTerminologyMappingMissingRequiredFields() throws Exception {
mockMvc.perform(post("/api/admin/terminology-mappings")
.contentType("application/json")
.content("""
{
"localCode": "",
"localSystem": "urn:workwell:demo",
"standardCode": "415070008",
"standardSystem": "http://snomed.info/sct"
}
"""))
.andExpect(status().isBadRequest());
}

@Test
void rejectsInvalidWaiverDates() throws Exception {
mockMvc.perform(get("/api/admin/waivers").param("expiresAfter", "not-a-date"))
Expand Down
27 changes: 27 additions & 0 deletions docs/JOURNAL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Journal

## 2026-05-20 — UAT Sections 9-14: Add Mapping UI, Studio packet selector, Demo Reset gating (issue #30)

**Goal:** Fix all reported Section 9 (Terminology Mappings), Section 11 (Audit Packets in Studio Release & Approval tab), and Section 14 (Reset Demo Data prod visibility) UAT bugs from GitHub issue #30, plus correct guide inaccuracies for Sections 9–14.

**Branch:** `fix/sprint-1-uat-sections-9-14`

**What changed:**

- `frontend/app/(dashboard)/admin/page.tsx` — Added "Add Mapping" form/dialog to the Local code mappings panel. The toggleable inline form posts to `POST /api/admin/terminology-mappings` and refreshes the table on success. Form validates required fields (local code/system, standard code/system) and confidence range (0.0–1.0). Also gated the Reset demo data card on `process.env.NEXT_PUBLIC_DEMO_MODE === "true"` so the card is structurally absent in production Vercel builds (production builds never set `NEXT_PUBLIC_DEMO_MODE=true` because `next.config.ts` fails fast if they do).
- `frontend/features/studio/components/ReleaseApprovalTab.tsx` — Replaced the legacy direct-JSON-download button with the shared `AuditPacketExportButton` so the Studio Release & Approval tab now exposes the same JSON/HTML format selector already used on case detail and runs. The third packet entry point is now consistent with the other two.
- `docs/WALKTHROUGH_GUIDE.md` — Corrected Sections 9–14 against the current UI: Section 9 renamed panel to "Local code mappings" and documented Reviewed By / Notes columns plus the new Add Mapping inline form (and noted that **Validate Mappings** lives on the **Source mappings** panel, not the terminology panel); Section 10 documented the actual button labels and called out that audit-events CSV export lives on the Cases page (not Admin); Section 11 documents the consistent JSON/HTML format dropdown across all three entry points (case detail, run detail panel, Studio Release & Approval tab); Section 12 added explicit Claude Desktop config-file path, bearer-token JSON snippet, and JWT acquisition instructions; Section 13 added a Bug 4 cross-reference for the `/login` redirect on session expiry; Section 14 documented the inline (non-modal) confirmation, the frontend visibility gate, and the seven-measure / four-lifecycle catalog left after a Demo Reset.

**Verification:**

- `frontend/npm run lint` — passed (only pre-existing `test/mocks/next-font.ts` anonymous-default-export warning).
- `frontend/npm test` — 40/40 passed.
- `frontend/npx tsc --noEmit` — clean (no TypeScript errors).
- Playwright local end-to-end against `localhost:3000` + `localhost:8080`:
- Logged in as `admin@workwell.dev`, navigated to `/admin`, opened the new **Add Mapping** form, submitted `ANNUAL-FIT-TEST` → SNOMED `415070008`, observed the new row appearing in the Local code mappings table and the form auto-closing.
- With `NEXT_PUBLIC_DEMO_MODE=true`: confirmed the **Reset demo data** card renders on `/admin`.
- Restarted the dev server with `NEXT_PUBLIC_DEMO_MODE=false`: confirmed the **Reset demo data** card no longer renders on `/admin` while the Admin page itself, including the Local code mappings panel and Add Mapping button, continues to render normally.
- Opened the Audiogram measure in Studio → Release & Approval tab, confirmed both the header and Release & Approval tab `Export Measure Audit Packet` controls expose JSON/HTML in the format dropdown (2 controls, both with `[json, html]` options).
- Manual /measures inspection — confirmed the seven seeded measures across four lifecycle states (4 Active, 1 Approved, 1 Draft, 1 Deprecated) matching the WALKTHROUGH_GUIDE.md Section 14 table.

---


## 2026-05-20 — UAT Sections 6-8: Run history, Studio, Admin fixes (issue #29)

**Goal:** Fix all reported Section 6 (Run History), Section 7 (Studio/CQL), and Section 8 (Admin panel) UAT bugs from GitHub issue #29.
Expand Down
Loading
Loading