As an integration consumer, I need the approval API to return the coded answers, so that systems outside Avni can read the reason a record was rejected.
Acceptance Criteria
Technical Details
-
avni-server-api/src/main/java/org/avni/server/web/response/EntityApprovalStatusResponse.java:8-16 — the response is a LinkedHashMap built with human-readable keys ("Entity ID", "Approval status comment", …). Add the observations after "Approval status comment", rendering them through Response.mapObservations(conceptRepository, conceptService, …) — the helper every other /api/* response already uses. It emits {"<question name>": "<resolved answer>"}, resolving coded answer UUIDs to concept names via ConceptService.getObservationValue and applying the API-version date normalisation.
Do not use ObservationService.constructObservations. An earlier version of this story named it and claimed it matched the other API responses; both halves were wrong. It is the web/DEA contract helper (EncounterService, IndividualService use it), it returns a list of ObservationContract rather than a name-keyed map, and it leaves coded values as raw UUIDs — so an integration reading /api/approvalStatuses would get a different shape from /api/subjects, which is the opposite of this story's point. It also opens two problems mapObservations avoids: a per-row concept lookup (N+1 across a page) and an NPE surface on Subject-datatype answers whose referenced subject is voided or out of RLS scope.
-
avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/EntityApprovalStatusWriter.java:26-30 — no code change expected. It calls entityApprovalStatusService.createStatus(...), which story 2 deliberately leaves alone, so rows arrive with null observations. This story is to prove that and add the regression test, not to change behaviour.
Do NOT:
- Do not block CSV import when an Approval form is mapped. That was considered and rejected in the spec: bulk import is an administrative action, and blocking it would break existing imports for any org that later attaches a form.
- Do not add observations to the CSV upload format. Supplying form answers per row is a much larger feature and is not in scope.
Testing Gotchas
/api/approvalStatuses is gated by @PreAuthorize("hasAnyAuthority('user')") and then checkApprovePrivilegeOnEntityApprovalStatuses, which throws if the caller lacks the approve privilege for any entity type in the page. A test user needs the right privileges or the endpoint 403s before serialisation is reached.
- The response keys contain spaces and are order-sensitive (
LinkedHashMap). Assert on content, not on a serialised string.
autoApprove reaches the writer as a @Value("#{jobParameters['autoApprove']}") String, parsed with Boolean.parseBoolean. Anything other than "true" is false — a common source of confusing test results.
avni-models
Epic: avniproject/avni-product#1901
As an integration consumer, I need the approval API to return the coded answers, so that systems outside Avni can read the reason a record was rejected.
Acceptance Criteria
GET /api/approvalStatusesincludes them for decisions that have them.autoApprove=truesucceeds and writes null observations.Technical Details
avni-server-api/src/main/java/org/avni/server/web/response/EntityApprovalStatusResponse.java:8-16— the response is aLinkedHashMapbuilt with human-readable keys ("Entity ID","Approval status comment", …). Add the observations after"Approval status comment", rendering them throughResponse.mapObservations(conceptRepository, conceptService, …)— the helper every other/api/*response already uses. It emits{"<question name>": "<resolved answer>"}, resolving coded answer UUIDs to concept names viaConceptService.getObservationValueand applying the API-version date normalisation.Do not use
ObservationService.constructObservations. An earlier version of this story named it and claimed it matched the other API responses; both halves were wrong. It is the web/DEA contract helper (EncounterService,IndividualServiceuse it), it returns a list ofObservationContractrather than a name-keyed map, and it leaves coded values as raw UUIDs — so an integration reading/api/approvalStatuseswould get a different shape from/api/subjects, which is the opposite of this story's point. It also opens two problemsmapObservationsavoids: a per-row concept lookup (N+1 across a page) and an NPE surface on Subject-datatype answers whose referenced subject is voided or out of RLS scope.avni-server-api/src/main/java/org/avni/server/importer/batch/csv/writer/EntityApprovalStatusWriter.java:26-30— no code change expected. It callsentityApprovalStatusService.createStatus(...), which story 2 deliberately leaves alone, so rows arrive with null observations. This story is to prove that and add the regression test, not to change behaviour.Do NOT:
Testing Gotchas
/api/approvalStatusesis gated by@PreAuthorize("hasAnyAuthority('user')")and thencheckApprovePrivilegeOnEntityApprovalStatuses, which throws if the caller lacks the approve privilege for any entity type in the page. A test user needs the right privileges or the endpoint 403s before serialisation is reached.LinkedHashMap). Assert on content, not on a serialised string.autoApprovereaches the writer as a@Value("#{jobParameters['autoApprove']}")String, parsed withBoolean.parseBoolean. Anything other than"true"is false — a common source of confusing test results.avni-models
Epic: avniproject/avni-product#1901