Task: Validate a saved document on the server

Table of Contents

This page documents a task in the Make every configuration screen follow one standard story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

Move the checks in the web's check() to the write path, so a client cannot save what the dialog refuses and every client gets the same answer. The errors move; the warnings and information findings stay on the screen and do not block a save.

2. Status

Field Value
State DONE
Parent story Make every configuration screen follow one standard
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-10-25

3. Acceptance

  • The analytics handler refuses a pricing engines document that breaks a rule, before it writes a row, and reports every problem it found.
  • Each problem carries a code from {name_missing, no_products, model_missing, engine_missing, parameter_repeated, parameter_value_missing}, the product id when it is about a product, the setting's scope and name when it is about a parameter, and a short message.
  • The save response carries the problems beside success and message, and the message counts them.
  • The BFF answers a refused save with 400 and the problems in the body. A version conflict stays 409 and a permission refusal stays 403.
  • The web dialog names the problems in a person's words, one line each.
  • The warnings and information findings of check() stay on the screen.

4. Plan

The protocol is generated from the operation model, so the problem struct and the field on the save response were added there and the C++ and TypeScript protocols regenerated.

The rules live in one pure function, ores::analytics::validation::problems_of, which the pricing engines save calls before it hands the document to the service. The handler also serves today's market documents, and each kind has its own subject and its own save, so only the pricing engines save is validated.

The BFF turns the problems into invalidRequest details, and the web turns them back into the findings check() produces, so one set of translated words says the same problem whichever side found it.

5. Measurements

What Value Command Commit
Analytics handler suite 12 cases, 93 assertions ores.analytics.core.tests "[configuration_document]" 8bf09e806e
Analytics core suite 76 cases, 75 pass ores.analytics.core.tests 8bf09e806e
BFF pricing suite 18 tests npx vitest run packages/bff/src/pricing-engines.test.ts 8bf09e806e
Web pricing suites 62 tests npx vitest run packages/web/src/pricingEngines 8bf09e806e
Latest pricing rows in the seeded data 23 configurations, 517 products, 1256 settings, none breaking a rule compass sql 8bf09e806e

6. Notes

  • The BFF reads a name before the service does, so a blank name is refused there: the server's name_missing rule is proven by the handler suite rather than through the BFF.
  • read_all_pricing_model_product_parameter_history in the analytics core suite fails on a freshly recreated database, and no commit on this branch touches it: the numeric value-set guard added at f1c17918b8 refuses the synthetic value with _v2 appended.

7. Test Scenarios

Scenario State Notes
     

8. PRs

PR Title
#3836 [analytics] Validate a pricing engines document before it is written

9. Review

Comment summary File Decision Notes
       

10. Result

A document with a product that names no model is refused through the BFF with 400 and {"code":"invalid-request","message":"The document has 1 problem.","problems":[{"code":"model_missing","productId":"...","message":"The Swap product has no model."}]}. A document whose product states the same setting twice and leaves its value empty is refused with 400 and two problems, parameter_repeated and parameter_value_missing, each naming the product, the scope and the setting. A valid document creates with 201 and amends with 200. A refused save writes no configuration header and no pricing row.

Emacs 29.3 (Org mode 9.6.15)