Plan · Formulation - Formulas : use a pre-mix as an ingredient, with its price, its (+) detail and "Not included" (frontend + backend + db)
Objective
A1: extend MP-690's composition aggregate in place. V035 lands the approved design's three columns, foreign key, five CHECKs and index on formula_phase_line with the matching entity fields; FormulaLineResponse loses kind - the work request's backend half, with the mapper constant and one test assertion - and gains premixFormulaId, premixCode, premixName, notIncluded and notIncludedLevel, as do the add and update requests; the six pre-mix rules, the mutually exclusive reference assignment and the single writer of formula_premix_use all live in the existing FormulaCompositionService; two reads are added, premix-candidates and premix-tree; then the mock pair is flipped to apiClient and its fixtures deleted. Reused unchanged: formula_premix_use and its daughters reader, the per-site price and INCI resolution, the client-side cost calculation and every pre-mix component. Nothing derived is stored and no replacement discriminator appears. Left out: a parallel pre-mix service or route (A2), stored prices or trees (A3), and the ticket's own exclusions.
Current state
Ready for reviewReady for your review in Jira (👤 Ready for Fullstack Review) since 12 hours.
Next action
A person: Ready for review — non-blocking recommendations are available.
Accepted route
Backend stage · approach A1 · next: Backend builder
accepted
Scope and acceptance criteria
User interface
- The flip of the accepted mock: api/formula-premix.ts calls apiClient only, and lib/mock-premix-data.ts is deleted with its three preview devices
- api/formula-composition.ts's line mirror gains premixFormulaId, premixCode, premixName, notIncluded and notIncludedLevel; nothing else in it changes and the discriminator does not come back
- use-formula-description reads the live GET /v1/formulas/{id}/daughters instead of the mock fixture, so the existing impact alert counts real formulas
- The three mock-stage limits the ticket requires gone disappear with the fixtures: a line can be added after a pre-mix line, no demo pre-mix lines are seeded into every formula, and the daughters alert lists only real formulas
- No component, no column definition and no cost calculation changes: the grid, the (+) rows, the level dialog, the badge and lib/composition-math.ts keep their current behaviour and their current reading of the served figures
- Every TODO(MP-691) marker disappears, each replaced by the apiClient call it named
Database
- One Flyway migration, V035, implementing the approved DB design's ALTER on formula_phase_line: three columns, one foreign key with an explicit ON DELETE RESTRICT, five named CHECKs and one index
- No new table, no reference-data entity and so no admin-console entry; formula_premix_use (V028) is reused with its shape, keys and indexes untouched and simply starts receiving rows
- formula_phase_line holds production rows: the one NOT NULL column carries a DEFAULT so the ALTER cannot abort, and nothing is narrowed, dropped, renamed or newly unique
- No kind column is added and none exists - the discriminator is the reference field, per the human work request and the design's own decision
- Entity changes land with the migration because ddl-auto is validate: three fields on FormulaPhaseLine plus a NotIncludedLevel enum
API and backend
- GET /v1/formulas/{formulaId}/composition: FormulaLineResponse loses kind and gains premixFormulaId, premixCode, premixName, notIncluded and notIncludedLevel; the composition query fetch-joins the referenced formula. Open to any authenticated user, as today
- POST /v1/formulas/{formulaId}/lines and PUT /v1/formulas/{formulaId}/lines/{lineId}: the request records gain premixFormulaId, notIncluded and notIncludedLevel; both references are applied mutually exclusively; six new rules refuse with their own message keys; both writes reconcile the pre-mix usage record. Authenticated user, behind the existing locked-composition refusal
- DELETE /v1/formulas/{formulaId}/lines/{lineId}: no contract change; reconciles the usage record after the delete, which is the half of AC10 that moves the count from 2 to 1
- GET /v1/formulas/premix-candidates?q=&siteId= (new, FormulaController): pre-mix formulas whatever their status, with code, name, status code/label/colour and the pre-mix's own total as entered. Authenticated user, mirroring the raw-material candidate endpoint
- GET /v1/formulas/{formulaId}/premix-tree?premixFormulaId=&siteId= (new, FormulaCompositionController): the referenced formula's ingredients to 5 levels with per-node share, name, INCI, per-site price/currency/unit and Not included pair. Authenticated user, read-only
- PUT /v1/formulas/{id}: no contract change, two behaviour changes - turning isPremix off clears the formula's line levels, turning it on is refused while a ticked line has no level; the existing flag-off guard also becomes effective now that the usage record is written
- GET /v1/formulas/{id}/daughters: unchanged code, now returning real rows - the alert stops reading zero
- POST /v1/formulas in duplicate mode: the composition copy carries the three new columns and then writes the duplicate's own usage rows; lot and batch stay deliberately uncopied
- New message keys in messages.properties and messages_fr.properties for the depth, cycle, both-references, not-a-pre-mix, level-required and batch-on-a-pre-mix-line refusals, plus the isPremix flip refusal; no new dependency and no new service, controller or repository beyond the two DTO families for the new reads
Acceptance criteria
- V035 matches the approved design's ALTER (three columns, FK with ON DELETE RESTRICT, five named CHECKs, one index); the entity carries the three fields so validate passes; the composition query fetch-joins the referenced formula.
- No kind component, no LINE_KIND_RAW_MATERIAL and no kind assertion remain, and no replacement appears; the response and both write requests carry the pre-mix trio and the Not included pair, named as the mock's types name them.
- Add and update refuse both references, a referenced formula that is not a pre-mix, depth over 5, a genuine cycle, a missing level on a pre-mix formula and a batch on a pre-mix line; a shared sub-pre-mix is accepted; the references are applied mutually exclusively; every key is in both files.
- After every add, update, delete and duplicate copy, the formula's formula_premix_use rows equal its lines' distinct pre-mix references; copyLines carries the three new columns with lot and batch still uncopied; the daughters endpoint reports 2 then 1.
- Turning the pre-mix flag off clears that formula's line levels; turning it on is refused while a ticked line has no level, with its own localized message; the existing refusal naming daughter codes still works.
- Pre-mix formulas are returned whatever their status, with code, name, status code/label/colour and the pre-mix's own total as entered; the live raw-material candidate endpoint is untouched.
- The walk returns every node to at most 5 levels with its share, the multiplied percentage, name, isPremix, INCI, per-site price/currency/unit and its Not included pair, resolved at the requested site through the existing per-site service.
- Each of AC1 to AC11 has a test naming it, each CHECK has its four cases, the two new endpoints have 401, validation and i18n coverage, and ./gradlew --no-daemon spotlessCheck and ./gradlew --no-daemon test pass.
- api/formula-premix.ts calls apiClient only with no TODO(MP-691) marker left, lib/mock-premix-data.ts is deleted, the line mirror carries the five new fields, the alert lists real formulas, the three mock-stage limits are gone, and npm run build and npm run lint pass.
- id: RC1 · check: V035 is the only new migration, its version is free on main, and it matches the approved design document on the branch line for line: three columns on formula_phase_line, one foreign key to formula with an explicit ON DELETE RESTRICT, five named CHECKs each with its IS NOT NULL guard, and one index on premix_formula_id. Nothing beyond the document is added, renamed or dropped, and…
- id: RC2 · check: not_included is NOT NULL with DEFAULT FALSE so the ALTER cannot abort on populated rows, not_included_level is constrained to THIS_PREMIX and FINAL_FORMULA and only when the flag is set, and formula_premix_use keeps its columns, keys, unique pair and indexes untouched. An empty line with neither reference is still accepted.
- id: RC3 · check: The entity matches the migration (ddl-auto is validate): premixFormula as a LAZY association, notIncluded non-null defaulting to false, notIncludedLevel mapped STRING with the two-value enum, and the composition query fetch-joins the referenced formula so the mapper never lazy-loads.
- id: RC4 · check: kind is gone from FormulaLineResponse, LINE_KIND_RAW_MATERIAL is gone from FormulaCompositionMapper, and the service test no longer asserts a line kind. No replacement discriminator, flag, computed value or constant of any name takes its place anywhere under backend/: the reference field is the only answer. The picker-label kind on raw-material candidates and the kind of lib/formu…
- id: RC5 · check: The line response carries premixFormulaId, premixCode, premixName, notIncluded and notIncludedLevel, and the two write requests carry premixFormulaId, notIncluded and notIncludedLevel as plain nullable components. Every new record component name matches the mock's own types at the branch head, so no hook and no component had to be adapted.
- id: RC6 · check: A line write applies the two references mutually exclusively - assigning a pre-mix clears the raw material and any picked batch, assigning a raw material clears the pre-mix - so a mixed edit returns the rule's message rather than a constraint violation, and a line holding both is refused.
- id: RC7 · check: Saving a composition that nests pre-mixes more than 5 levels deep is refused, and a formula that would contain itself directly or through other pre-mixes is refused; a sub-pre-mix legitimately reached twice by different paths is ACCEPTED. Both refusal texts are the preview's wording verbatim, in messages.properties and messages_fr.properties.
- id: RC8 · check: Referencing a formula whose is_premix is false is refused; a pre-mix line accepts a typed lot only and never a picked batch; on a formula that is itself a pre-mix a ticked line cannot be saved without a level, and on any other formula the level must be null. Every new message key exists in both properties files.
- id: RC9 · check: One writer of formula_premix_use: after an add, an update, a delete and a duplicate copy, the formula's rows equal the distinct pre-mix references of its lines. Adding a pre-mix to two formulas makes the daughters endpoint report 2, and removing it from one makes it report 1, with no change to the daughters query or to the flag-off guard.
- id: RC10 · check: copyLines carries premix_formula_id, not_included and not_included_level while still leaving lot and batch uncopied, and the duplicate gets its own usage rows - a duplicated formula's pre-mix lines are not rows with neither reference.
- id: RC11 · check: GET /v1/formulas/premix-candidates returns pre-mix formulas whatever their status, including Development, with code, name, status code/label/colour and the pre-mix's own total as entered, never rescaled; the live raw-material candidate endpoint is untouched and the two lists are merged in the data layer, not replaced.
- id: RC12 · check: GET /v1/formulas/{formulaId}/premix-tree walks the referenced formula to at most 5 levels and serves per node its share, name, whether it is itself a pre-mix, its INCI and its pricePerUnit, currencyCode and unitCode resolved at the requested siteId through the existing per-site service. A 50/50 pre-mix of raw materials at 2.00 and 4.00 EUR/kg at site S yields 3.00 EUR/kg, and the…
- id: RC13 · check: Nothing derived is stored or served as a verdict: no price, reason, tree, percentage, total, code, name or INCI column exists, and the dash with its reason plus the footer's excluded count remain the client-side cost calculation's result from the served price, currency and unit. A USD ingredient, an unpriced ingredient and a per-litre ingredient each produce the dash with the rea…
- id: RC14 · check: Turning isPremix off clears the formula's line levels; turning it on is refused, with its own localized message, while a ticked line has no level. The existing refusal to clear the flag while daughters consume the formula still names the daughter codes.
- id: RC15 · check: lib/mock-premix-data.ts is deleted, api/formula-premix.ts contains no fixture import and no TODO(MP-691) marker, and every one of its functions calls apiClient. A rule refusal still arrives as the existing refused result, so the toast-and-revert path and the one-cell revert are unchanged.
- id: RC16 · check: The three mock-stage limits are gone on the preview: a line can be added after a pre-mix line, no demo pre-mix line is seeded into any formula (a formula's total is its own again), and the daughters alert lists only real formulas.
- id: RC17 · check: No behaviour the PO accepted has changed: a pre-mix line's INCI cell is empty, its Lot takes free text with no batch suggestions, the (+) shows ingredients read-only with their INCI and their percentage in the final formula and opens nested pre-mixes to 5 levels with nothing editable, and a pre-mix totalling 95% is used as entered with its warning.
- id: RC18 · check: lib/composition-math.ts, lib/premix-rules.ts, the single pre-mix predicate, premix-detail-rows.tsx, not-included-level-dialog.tsx, premix-badge.tsx, premix-impact-alert.tsx and the column definitions are not rewritten: the flip changed the data source, not the behaviour. Existing estimateCost exclusion reasons are added to, never renamed.
- id: RC19 · check: The verification gate passes on the branch head: ./gradlew --no-daemon spotlessCheck and ./gradlew --no-daemon test in backend/, then npm run build and npm run lint in frontend/. Each of AC1 to AC11 has at least one test naming it, and the two new endpoints have 401, validation and i18n coverage in a Keycloak and Postgres controller IT.
- id: RC20 · check: Scope discipline: no second composition service, controller family or cost calculation; no new npm or Gradle dependency; no change to how a formula is marked as a pre-mix; no currency or unit conversion; no combined INCI list for the finished product; no all-levels-expanded view; and no change to formula_premix_use's shape, to rawmaterial_site or to the live raw-material search.
Assumptions and constraints
- Implement the approved DB design exactly as approved: no column, CHECK, index or foreign key beyond it, and no change to formula, formula_phase, formula_premix_use, rawmaterial, rawmaterial_batch or rawmaterial_site
- No line discriminator anywhere: kind leaves the response, the mapper constant and the test assertion, and no replacement field, flag or computed value takes its place
- Nothing derived is stored: no price, reason, tree, percentage, total, code, name or INCI column
- One composition service, one composition controller surface and one cost calculation; no parallel route or service for pre-mix lines
- Response records must match the shapes the branch's mock api returns, so the flip changes no hook and no component; read them at the branch head before authoring the DTOs
- Every new messageKey exists in both messages.properties and messages_fr.properties, and the depth, cycle and both-references wording is the preview's wording verbatim
- V035 is the next free version at the indexed base; confirm it against main before authoring the file
- The live raw-material candidate search and POST /v1/raw-materials/site-data are reused untouched
- No new npm or Gradle dependency
- Out of scope for this ticket: the all-levels-expanded view (MP-440), the finished product's combined INCI list, regulatory documents reading Not included, currency or unit conversion, and any change to how a formula is marked as a pre-mix
Risks, dependencies and expected result
- R1The exact component names of the pre-mix tree and candidate records are not enumerable from here: the branch commits are not in the read-only checkout, so only the overlay excerpts of the mock's types were readable. A mismatch would force a component change at the flip.
Mitigation: Slices 6 and 7 require the DTO components to be read off the mock's PremixTree, PremixIngredientNode and PremixLinePrice types at the branch head and matched exactly; RC5 checks that no hook or component had to be adapted.
- R2V035 may be taken by another ticket's migration landing on main before this one is authored; the index is exact today but main moves.
Mitigation: Slice 1 re-checks the next free version against main before authoring and renumbers if needed; the file name is the only thing that changes.
- R3The line response changes shape for an endpoint already merged on main - kind removed and five components added - in one release, so any other consumer of that response would see the change.
Mitigation: The only consumer is the composition grid's own data layer, which is flipped in the same branch; responses are not schema-validated by the shared client, and the H2 and controller ITs cover the new shape.
- R4The approved design left seven decisions to the reviewer, and the gate was passed without written answers to them - notably whether a pre-mix line may be the QSP line, whether a duplicate keeps its pre-mix lines, and the level handling on an isPremix flip.
Mitigation: Each is implemented as the approved document states it (allowed, kept, and the stated flip behaviour), recorded in decisions D4 and D5 so a reviewer can overrule one as a contained change rather than a redesign.
- R5A pre-mix line's price is walked level by level at read time, which multiplies queries on deep nesting and could be slow on a composition with many pre-mix lines.
Mitigation: The new index on premix_formula_id supports the walk, the depth is capped at 5, and the per-site resolution is already a batch call; caching would be a later ticket, not a column.
- R6The design's open item on a pre-mix whose ingredient has no typed percentage, or which is over-filled, would be priced low rather than showing the dash, because the served figures carry no way to say refused. The ticket states no criterion for that case.
Mitigation: Kept as the accepted behaviour: the client verdict is unchanged and AC4's three stated cases are served by price, currency and unit. Recorded as decision D6 so the Fullstack Review can raise it as a product question.
- R7Removing the discriminator and extending the line contract in the same slices as the new rules could let a replacement discriminator slip in under another name, or let a renamed exclusion reason change AC4's wording.
Mitigation: RC4, RC13 and RC18 check for any replacement discriminator under backend/ and that the existing exclusion reasons and the cost calculation are added to, never renamed.
Expected result
Backend builder takes over the Backend stage from this plan.
Milestones
- 1Migration V035 and the line entity (db-engineer)
Backend builder
Done when: V035 matches the approved design's ALTER (three columns, FK with ON DELETE RESTRICT, five named CHECKs, one index); the entity carries the three fields so validate passes; the composition query fetch-joins the referenced formula.
- 2Line contract: drop kind, add the pre-mix and Not included components (backend-engineer)
Backend builder · after step 1
Done when: No kind component, no LINE_KIND_RAW_MATERIAL and no kind assertion remain, and no replacement appears; the response and both write requests carry the pre-mix trio and the Not included pair, named as the mock's types name them.
- 3The six write rules and their messages (backend-engineer)
Backend builder · after step 2
Done when: Add and update refuse both references, a referenced formula that is not a pre-mix, depth over 5, a genuine cycle, a missing level on a pre-mix formula and a batch on a pre-mix line; a shared sub-pre-mix is accepted; the references are applied mutually exclusively; every key is in both files.
- 4Pre-mix usage record: one writer and its reconciliation (backend-engineer)
Backend builder · after step 3
Done when: After every add, update, delete and duplicate copy, the formula's formula_premix_use rows equal its lines' distinct pre-mix references; copyLines carries the three new columns with lot and batch still uncopied; the daughters endpoint reports 2 then 1.
- 5Not included level across the isPremix flip (backend-engineer)
Backend builder · after step 3
Done when: Turning the pre-mix flag off clears that formula's line levels; turning it on is refused while a ticked line has no level, with its own localized message; the existing refusal naming daughter codes still works.
- 6GET /v1/formulas/premix-candidates (backend-engineer)
Backend builder · after step 2
Done when: Pre-mix formulas are returned whatever their status, with code, name, status code/label/colour and the pre-mix's own total as entered; the live raw-material candidate endpoint is untouched.
- 7GET /v1/formulas/{formulaId}/premix-tree (backend-engineer)
Backend builder · after step 2
Done when: The walk returns every node to at most 5 levels with its share, the multiplied percentage, name, isPremix, INCI, per-site price/currency/unit and its Not included pair, resolved at the requested site through the existing per-site service.
- 8Tests per criterion and the backend gate (backend-engineer)
Backend builder · after step 4, 5, 6, 7
Done when: Each of AC1 to AC11 has a test naming it, each CHECK has its four cases, the two new endpoints have 401, validation and i18n coverage, and ./gradlew --no-daemon spotlessCheck and ./gradlew --no-daemon test pass.
- 9Flip the mock pair to the API and delete the fixtures (frontend-engineer)
Backend builder · after step 8
Done when: api/formula-premix.ts calls apiClient only with no TODO(MP-691) marker left, lib/mock-premix-data.ts is deleted, the line mirror carries the five new fields, the alert lists real formulas, the three mock-stage limits are gone, and npm run build and npm run lint pass.
Technical details
Decision, design and alternatives3
- Selected
- A1
- Architecture
- TARGET, LAYER BY LAYER. Everything below is on the existing ticket branch; the approved design document on the branch is the normative source for the DDL and must not be redesigned (E25). 1. SCHEMA (slice 1, db-engineer). One Flyway file, V035__Add_Premix_Reference_And_Not_Included_To_Formula_Phase_Line.sql, transcribed from the design document's ALTER on formula_phase_line: premix_formula_id UUID NULL with a foreign key to formula(id) and an explicit ON DELETE RESTRICT; not_included BOOLEAN NOT NULL DEFAULT FALSE (the DEFAULT is what lets the ALTER run on populated rows); not_included_level VARCHAR(20) NULL. Five named CHECKs, each with its IS NOT NULL guard: never both references, no direct self-reference (premix_formula_id <> formula_id), no picked batch on a pre-mix line, the level restricted to THIS_PREMIX and FINAL_FORMULA, and a level only when the flag is set. One index, idx_formula_phase_line_premix_formula_id, which the RESTRICT check, the tree walk and the usage reconciliation all need. Nothing else: no kind column (the work request and design decision D2), no new table, no reference-data entity. V034 is the highest version at the indexed base (E30); the builder confirms the number against main before authoring. 2. ENTITIES (slice 1). FormulaPhaseLine gains premixFormula (@ManyToOne LAZY on premix_formula_id, to Formula), notIncluded (@Builder.Default false, nullable=false) and notIncludedLevel (@Enumerated(STRING), length 20), plus a new enum NotIncludedLevel {THIS_PREMIX, FINAL_FORMULA} in domain/formula - the two codes the mock already exchanges (E22). ddl-auto is validate, so the fields and the migration land together (E6). findCompositionByFormulaId adds LEFT JOIN FETCH on the referenced formula so mapping never lazy-loads (E32). 3. READ CONTRACT (slice 2). FormulaLineResponse drops kind and gains premixFormulaId (UUID, null on a raw-material or empty line), premixCode and premixName (String, "" when absent, matching the record's existing "" convention for commercialName), notIncluded (Boolean) and notIncludedLevel (String code or null). FormulaCompositionMapper loses LINE_KIND_RAW_MATERIAL and fills the pre-mix trio from line.getPremixFormula() (E3, E4). FormulaCompositionServiceTest loses the one kind assertion (E5). This completes the human work request's backend half; no replacement discriminator of any name is introduced anywhere - the reference field is the answer. 4. WRITE CONTRACT (slices 2-3). UpdateFormulaLineRequest gains premixFormulaId, notIncluded and notIncludedLevel as plain nullable components, following the record's documented full-replacement carve-out (E20). AddFormulaLineRequest gains the same three so a line can be created already holding a pre-mix. applyValues is extended to apply the two references mutually exclusively - assigning a pre-mix clears the raw material and the picked batch, assigning a raw material clears the pre-mix - so a mixed edit returns the rule's message instead of hitting the CHECK (E13). 5. RULES (slice 3), all in FormulaCompositionService, each a BusinessRuleException with a new messageKey: (a) a line holds at most one reference, with the preview's wording; (b) the referenced formula exists and is_premix is true; (c) nesting depth at most 5, walking the referenced formula's lines; (d) no cycle - a depth-first walk from the edited formula over premix references that accepts a shared sub-pre-mix reached twice by different paths and refuses only a genuine loop, which is the behaviour the mock review demanded; (e) on a formula that is itself a pre-mix, a ticked line requires a level, and on any other formula the level must be null; (f) a pre-mix line carries a typed lot only, never a picked batch. The depth, cycle and both-references messages reproduce the preview's text verbatim (E21) so the message does not change between the optimistic refusal and the server's, in messages.properties and messages_fr.properties next to the existing exception.formula.line.* keys (E29). Errors propagate to GlobalExceptionHandler as ProblemDetail; nothing is caught broadly. 6. USAGE RECORD (slice 4). One writer: a private reconcile(formulaId) in FormulaCompositionService that makes the formula's formula_premix_use rows equal the distinct non-null premixFormulaId values of its lines, using findByFormula_Id (E9). Called after addLine, updateLine, deleteLine and after copyComposition. copyLines' explicit column list gains premix_formula_id, not_included and not_included_level, keeping lot and batch deliberately uncopied (E12), and copyComposition then reconciles the duplicate (E14). GET /v1/formulas/{id}/daughters and the flag-off guard are untouched and start reporting the truth (E10, E11), which is AC10 end to end and what makes the alert count move from 2 to 1. 7. THE isPremix FLIP (slice 5), in FormulaService#applyPremixChange (E11): turning the flag off clears notIncludedLevel on that formula's lines, since the level is meaningless there; turning it on is refused, with its own message key, while a ticked line has no level. This is the behaviour the approved design states for its open item on the flip; it is adopted, not invented here. 8. READ ENDPOINTS (slices 6-7). - GET /v1/formulas/premix-candidates?q=&siteId= on FormulaController, served by FormulaService over formula.is_premix (E19, E31): no status filter, so a pre-mix in Development is offered (AC1); each row carries code, name, status code, label and colour and the pre-mix's own total as entered, computed as the sum of its filled non-QSP line percentages plus a filled QSP line's derived share. It mirrors the raw-material candidate endpoint's parameters and ranking (E17) so the grid merges two ranked lists exactly where the mock merges them today (E24); the live raw-material search is not touched. - GET /v1/formulas/{formulaId}/premix-tree?premixFormulaId=&siteId= on FormulaCompositionController (E15), served by FormulaCompositionService: a depth-first walk of the referenced formula's lines capped at 5 levels, each node carrying its path, depth, share in its own pre-mix, the referenced name, whether the node is itself a pre-mix, its aggregated INCI, its pricePerUnit, currencyCode and unitCode at the requested site, and its own notIncluded pair. {formulaId} is the formula being edited and siteId is the site it is being read at, so the same pre-mix prices differently at another site (AC2, AC3); prices and INCI come from RawMaterialSiteDataService, which already resolves exactly these fields per site (E18). The percentage in the final formula is the line's percentage multiplied along the path, as the mock computes it. - The server serves figures, not verdicts: the dash with its reason and the excluded count stay in the one client-side cost calculation that already owns MP-690's exclusion reasons (E26), so AC4's foreign-currency, no-price and wrong-unit cases are the existing reasons applied to pre-mix nodes, and AC5's not-100% warning stays a client-side reading of the served total. 9. THE FLIP (slice 9, frontend-engineer). api/formula-premix.ts becomes apiClient calls against the endpoints its TODO(MP-691) markers name, keeping its refused-result shape by mapping a 400 ProblemDetail detail into { outcome: "refused", message } so the hook's toast-and-revert path and every component stay as they are (E24). lib/mock-premix-data.ts is deleted with its three preview devices, which is what removes the three mock-stage limits the ticket requires gone: a line can be added after a pre-mix line, no demo lines are seeded into every formula, and the daughters alert lists only real formulas (E23, E27). api/formula-composition.ts's line mirror gains the five new fields and loses nothing else; use-formula-description reads the live daughters endpoint instead of the fixture. The single pre-mix predicate over premixFormulaId stays the only derivation site (E28). 10. TESTS (slice 8). FormulaCompositionServiceTest for each rule and for the reconciliation; FormulaServiceTest for the flip; a repository H2 IT for the migration and its five CHECKs, following the existing H2 IT naming; a Keycloak and Postgres controller IT for the two new endpoints covering 401, validation and the i18n of each new message. The gate is ./gradlew --no-daemon spotlessCheck and ./gradlew --no-daemon test, then npm run build and npm run lint after the flip (AC13). DATA FLOW. Grid cell search -> formula-candidates plus premix-candidates, merged client-side -> PUT or POST on the line with premixFormulaId -> service rules -> line row plus reconciled usage rows -> composition response with the pre-mix trio -> (+) opens premix-tree at the current site -> client cost calculation renders the price, the dash with its reason, the footer count and the not-100% warning. ERROR HANDLING. Rule refusals are BusinessRuleException (400) with a messageKey present in both message files; a missing formula, line or referenced pre-mix is ResourceNotFoundException (404); every composition write stays behind the existing locked-status refusal (E14). Nothing new is caught or swallowed in the api layer: a refusal becomes the mock's refused result, any other failure keeps the existing error path.
- Rationale
- The code leaves little room for anything else and the two earlier stages fixed the rest. There is exactly one composition line table, one line entity, one mapper, one write service and one locked-formula guard (E6, E7, E13, E14, E15), so a second family (A2) would duplicate the ordering, locking and audit logic and split the single-reference rule across two writers. The approved DB design stores nothing derived and was approved on that basis (E25), prices are per-site and already resolved per site (E17, E18), and AC3 makes that site-dependence observable - so A3 would both contradict the approved schema and be wrong at the second site. The accepted mock is the API contract: two level codes (E22), a depth of 5 with three exact messages the backend must reproduce (E21), a refused-result error shape and one TODO(MP-691) marker per function naming its endpoint (E24); A1 is the only shape that lets the flip change no hook and no component. AC10 needs the usage record written, not redesigned: the table, its repository and its only reader already exist (E8, E9, E10) and the flag-off guard becomes effective the moment rows appear (E11). Finally, the human work request's backend half is unavoidably part of this stage: kind is still on the response, in the mapper constant and in one test assertion at the base (E1, E2, E3, E4, E5), and no routine before this one may touch backend/.
Design by area
User interface
This is a flip, not a redesign. The mock pair is the only place that knows the data is fake (E23, E24), which is what makes the change one greppable pair of files plus the line mirror.
1. api/formula-premix.ts. Each exported function becomes the apiClient call its TODO(MP-691) marker names: the pre-mix half of the ingredient search to GET /v1/formulas/premix-candidates, the (+) tree to GET /v1/formulas/{formulaId}/premix-tree with the pre-mix and the current site, and the daughters read to the live endpoint. searchCompositionCandidates keeps its current structure: it already reuses MP-690's live raw-material search unchanged and merges the pre-mix half into the ranked result (E24), so only the pre-mix half's source changes and the merge stays where it is. The refused-result contract is preserved: a 400 ProblemDetail becomes { outcome: "refused", message: <detail> }, so the hook's existing toast-and-revert path and the one-cell revert need no new branch, and a thrown error stays reserved for a genuine network or server failure. The site-slot device goes: the real siteId is passed through, which is what makes AC3 real rather than simulated.
2. lib/mock-premix-data.ts is deleted. Nothing else imports it, and its three devices go with it (E23): price slots assigned in first-seen order, one demo pre-mix line seeded into every formula at 1% in its own phase, and a usage record derived from the formula's own lines. Deleting it is what removes the ticket's three mock-stage limits and restores every formula's own total.
3. api/formula-composition.ts. The local line-response mirror and the line mapping gain the five new fields so the grid reads them from the live composition; the endpoint, its path and every other mapped field stay as they are, and no discriminator is reintroduced in any form.
4. use-formula-composition.ts. The mock overlay merge is removed where it injected mocked lines and line state, leaving the hook as the single owner of the composition's data reading one live response. Its refusal contract and its optimistic revert are unchanged.
5. use-formula-description.ts. listMockDaughters is replaced by the live daughters read; components/premix-impact-alert.tsx is not modified, and the page's refresh on the set of premixFormulaId values keeps working as written (E27).
6. Untouched on purpose: lib/composition-math.ts (the one cost calculation, including the dash with its reason, the exclusion reasons and the footer count), lib/premix-rules.ts (kept as the optimistic preview whose messages now match the server's word for word), the single pre-mix predicate over premixFormulaId (E28), premix-detail-rows.tsx, not-included-level-dialog.tsx, premix-badge.tsx, the column definitions and every page and route. If a served field name differs from the mock's, the DTO is renamed on the backend side rather than the component adapted - the mock is the contract.
7. Gate: npm run build and npm run lint in frontend/, then a pass on the preview against a real pre-mix - the price at two sites, the dash cases, the not-100% warning, the (+) tree to 5 levels, the empty INCI cell, the free-text lot, the two refusals and the level question - which is also what the Fullstack Review judges for AC-E1 and AC-E2.Database
The schema work is transcription, not design: the DB stage's document on the branch is the normative source and was approved at DB Review; this stage authors the migration it deliberately left out (E25).
FILE. backend/applications/cosmoreg-core/src/main/resources/db/migration/V035__Add_Premix_Reference_And_Not_Included_To_Formula_Phase_Line.sql. V034 is the highest version at the indexed base (E30), so V035 is next free; the builder re-checks the number against main before authoring, because another ticket's migration may have landed meanwhile.
COLUMNS on formula_phase_line (E6, E7).
- premix_formula_id UUID NULL, foreign key to formula(id) with an explicit ON DELETE RESTRICT, matching the table's existing raw-material and batch foreign keys and the guarantee that a pre-mix still used by a line cannot be deleted.
- not_included BOOLEAN NOT NULL DEFAULT FALSE. The DEFAULT is what makes the ALTER safe on populated rows; every existing line reads false, which is the behaviour before this ticket.
- not_included_level VARCHAR(20) NULL, a code set fixed by a CHECK rather than reference data - the design's deliberate, reviewed deviation, approved at the gate.
CHECKS, each named and each carrying its IS NOT NULL guard so it never fires on a legal NULL (the pattern the table already uses, E7).
- chk_formula_phase_line_single_reference: a line never holds both references. 'Neither' stays legal, because MP-690 creates an empty line with no raw material, and no new constraint is added for it - the ticket's reference rule and the work request both say so.
- no direct self-reference: premix_formula_id IS NULL OR premix_formula_id <> formula_id. The indirect cases cannot be a row-level CHECK and stay service rules.
- no picked batch on a pre-mix line: premix_formula_id IS NULL OR rawmaterial_batch_id IS NULL, which is the Lot-is-free-text-only rule for a pre-mix line.
- the level's value set: not_included_level IS NULL OR not_included_level IN ('THIS_PREMIX', 'FINAL_FORMULA') - exactly the two codes the mock exchanges (E22).
- the level needs the flag: not_included_level IS NULL OR not_included.
For each CHECK the file states its allowed, rejected-by-value, rejected-by-NULL and unaffected cases, as the design document does.
INDEX. idx_formula_phase_line_premix_formula_id. The table's existing indexes lead with formula_id, rawmaterial_id and rawmaterial_batch_id, so none serves a lookup by the new reference, which the foreign key's RESTRICT check, the level-by-level price walk and the usage reconciliation all perform.
REUSED UNCHANGED. formula_premix_use (V028) keeps its columns, its two foreign keys, its unique pair and its two indexes (E8); this stage gives it its single writer in the service layer, which is what makes the existing daughters reader and the flag-off guard truthful (E9, E10, E11). formula.is_premix and its partial index are untouched and simply queried by the candidate endpoint (E31). rawmaterial_site keeps serving per-site prices (E18).
NOT IN THE SCHEMA, deliberately. A line's resolved price and its reason, a node's share, the percentage in the final formula, a pre-mix's total, its code, name and INCI: all read-time results. The 5-level depth limit, indirect cycles, 'neither reference on a filled line', 'the referenced formula is a pre-mix' and 'the level is required on a pre-mix formula' span several rows or tables and stay service rules, each with the message the design lists.
ENTITY MAPPING (ddl-auto is validate, so it ships with the migration). FormulaPhaseLine gains premixFormula as a LAZY @ManyToOne on premix_formula_id, notIncluded as a non-null Boolean defaulting to false, and notIncludedLevel as @Enumerated(STRING) with length 20; the new enum NotIncludedLevel {THIS_PREMIX, FINAL_FORMULA} lives in domain/formula. The composition fetch join adds the referenced formula so the mapper never lazy-loads (E32).
VERIFICATION. A repository H2 integration test, following the existing H2 IT naming in repository/formula, asserts each CHECK's four cases, the RESTRICT behaviour and that an existing row reads not_included false after the migration.API and backend
One aggregate, one service, one controller surface. The pre-mix line is a composition line, so every write and read below extends the classes MP-690 merged (E13, E14, E15); nothing parallel is created.
DTOs.
- FormulaLineResponse: remove kind; add premixFormulaId (UUID, null unless the line references a pre-mix), premixCode and premixName (String, "" when absent, following the record's existing empty-string convention), notIncluded (Boolean), notIncludedLevel (String code or null). This completes the human work request's backend half together with the mapper constant and the test assertion (E1, E2, E3, E4, E5).
- UpdateFormulaLineRequest and AddFormulaLineRequest: add premixFormulaId, notIncluded, notIncludedLevel as plain nullable components, following the update record's documented full-replacement carve-out (E20); isQsp keeps its @NotNull.
- PremixCandidateResponse (new record): formulaId, code, name, statusCode, statusLabel, statusColorCode, totalPercentage.
- PremixTreeResponse and PremixTreeNodeResponse (new records): the tree for one pre-mix line plus one node per ingredient, each with path, depth, its share in its own pre-mix, name, isPremix, inci, pricePerUnit, currencyCode, unitCode, notIncluded and notIncludedLevel. The component names must be read off the mock's PremixTree, PremixIngredientNode and PremixLinePrice types at the branch head and matched exactly, so the flip changes no hook or component (E24, E26).
MAPPER. FormulaCompositionMapper drops LINE_KIND_RAW_MATERIAL and fills the pre-mix trio from line.getPremixFormula(), keeping the rule that it reads only initialised associations - so the composition query fetch-joins the referenced formula (E4, E32). It stays a static utility; the two new read shapes are mapped by the same class or by a sibling static mapper in mapper/formula, as the area's convention requires.
SERVICE: FormulaCompositionService (@Transactional, readOnly for reads). New private validation, invoked by addLine and updateLine before anything is written, so a refusal leaves the composition untouched exactly as the existing rules do:
1. at most one reference per line (message: the preview's both-references wording);
2. the referenced formula exists (404) and its is_premix is true (400);
3. depth at most 5 over the referenced formula's own pre-mix lines;
4. no cycle: a depth-first walk from the edited formula that accepts the same sub-pre-mix reached by two different paths and refuses only a genuine loop - the behaviour the mock review explicitly demanded;
5. the Not included level is required on a line of a formula whose is_premix is true and must be null otherwise;
6. a pre-mix line carries a typed lot only, never a picked batch.
Each throws BusinessRuleException with a messageKey; the depth, cycle and both-references texts reproduce the preview's wording verbatim (E21) so the user sees the same sentence from the optimistic check and from the server. applyValues applies the two references mutually exclusively - a pre-mix clears the raw material and the batch, a raw material clears the pre-mix - so a mixed edit returns the message rather than hitting the CHECK (E13).
USAGE RECORD, single writer. A private reconcile(formulaId) makes the formula's formula_premix_use rows equal the distinct non-null premixFormulaId values of its lines, reading findByFormula_Id and deleting or inserting the difference (E9). Called at the end of addLine, updateLine, deleteLine and copyComposition. copyLines' explicit column list gains the three new columns, lot and batch staying uncopied (E12), and copyComposition reconciles the duplicate afterwards (E14). Nothing else changes for AC10: the daughters endpoint and the flag-off guard are untouched (E10, E11).
FORMULA SERVICE. applyPremixChange gains the level handling for the flip (E11): on true-to-false, clear notIncludedLevel on that formula's lines; on false-to-true, refuse with its own key while a ticked line has no level. findPremixCandidates(q, siteId) reads formulas with is_premix true and no status filter (E19, E31), ranks them like the raw-material candidate search (E17), and computes each pre-mix's own total as the sum of its filled non-QSP line percentages plus a filled QSP line's derived share - as entered, never rescaled, which is what AC5 relies on.
PRE-MIX TREE. FormulaCompositionService#getPremixTree(formulaId, premixFormulaId, siteId) walks the referenced formula's lines level by level, capped at 5, refusing nothing and simply not walking past the cap; it resolves each leaf's name, INCI, price, currency and unit through RawMaterialSiteDataService at the requested siteId (E18), and multiplies the shares along the path so each node also carries its percentage in the final formula. {formulaId} is the formula being edited and siteId the site it is read at, which is what makes the same pre-mix price differently at another site (AC2, AC3). The service serves figures only: the dash with its reason, the excluded count in the footer and the not-100% warning stay in the one client-side cost calculation that already owns MP-690's exclusion reasons (E26).
CONTROLLERS. FormulaCompositionController gains one GET /premix-tree; FormulaController gains one GET /premix-candidates, placed before the /{id} mapping's resolution order. Both are reads open to any authenticated user, like the composition endpoints around them (E15, E16); neither changes an existing signature apart from the request records above.
ERRORS AND i18n. Every refusal is a BusinessRuleException (400) or ResourceNotFoundException (404) reaching GlobalExceptionHandler as a ProblemDetail; no broad catch. Every new key is added to messages.properties and messages_fr.properties beside the existing exception.formula.line.* family (E29).
TESTS. FormulaCompositionServiceTest: one test per rule, the reconciliation on add, update and delete, and the mutually exclusive references; the kind assertion is removed. FormulaServiceTest: the two flip behaviours and the pre-mix candidate query with a Development pre-mix. A repository H2 IT for the migration and its CHECKs. A Keycloak and Postgres controller IT for the two new endpoints: 401 unauthenticated, parameter validation, and the localized text of each new message. Gate: ./gradlew --no-daemon spotlessCheck and ./gradlew --no-daemon test.Alternatives considered
- A1 — Extend MP-690's composition aggregate in place. The approved design's three columns land on formula_phase_line as V035 with the entity fields; FormulaLineResponse loses kind and gains the pre-mix trio and the Not included pair; the line write requests gain the same components; every new rule and the pre-mix usage reconciliation live in the existing FormulaCompositionService; two read endpoints are added, GET /v1/formulas/premix-candidates on the formula controller and GET /v1/formulas/{formulaId}/premix-tree on the composition controller; prices and INCI come from the existing per-site resolut…
Pros: One home for a composition line, one cost calculation, one locked-formula guard - the ticket's live-analogue rule; The response records stay the shapes the accepted mock already returns, so the flip changes no hook and no component; Reuses the usage record, its repository and its reader unchanged: AC10 needs rows, not a redesign; Implements the approved DB design exactly as approved, with no schema decision reopened; The per-site price precedent of MP-690 answers AC2 and AC3 with no new stored value. Cons: FormulaCompositionService grows: the pre-mix rules, the tree walk and the usage reconciliation all attach to it; A pre-mix line's price is walked level by level at read time, which multiplies queries on deep nesting; The line response changes shape for a merged endpoint in the same release as the discriminator removal.
- A2 — Add a parallel pre-mix composition family: a PremixCompositionController and service under /v1/formula-premix-lines owning the pre-mix lines, their rules, their tree and the usage record, leaving MP-690's composition endpoints untouched apart from reading the new columns.
Pros: Keeps the merged MP-690 service and its tests almost unchanged; The new rules sit in a class of their own. Cons: A second home for the composition line: two writers of one table, two lock guards, two audit paths and two orderings to keep in step; The grid would write a line through one route and a pre-mix line through another, which the accepted mock does not do - it writes every line through one data layer; Both-reference and single-reference rules would straddle two services, so a mixed edit could satisfy each one separately; Rejected by the ticket's live-analogue rule: extend the grid, its data layer and its cost calculation, do not build a second one.
- A3 — Store the derived pre-mix figures - a line's resolved price and reason, each node's share and the pre-mix's total - in new columns or a side table written on save, so the grid reads them without a walk.
Pros: One read per composition however deep the nesting; The cost footer would need no client-side arithmetic. Cons: Contradicts the approved DB design, which stores nothing derived and was approved on that basis; A price is site-dependent: a stored figure is wrong for every other industrial site, which AC3 exercises directly; Every edit to any pre-mix at any level would have to invalidate the stored figures of every formula above it; Reopens a settled schema decision at the stage that implements it.
API19
API contract
User interface
The frontend consumes exactly the shapes of the backend-plan's apiContract: the composition line without kind and with the pre-mix trio and the Not included pair, GET /v1/formulas/premix-candidates for the pre-mix half of the ingredient search, GET /v1/formulas/{formulaId}/premix-tree?premixFormulaId=&siteId= for the (+) detail, and the unchanged GET /v1/formulas/{id}/daughters for the impact alert. A rule refusal arrives as a 400 ProblemDetail and is surfaced as the existing { outcome: "refused", message } result.API and backend
GET /v1/formulas/{formulaId}/composition -> FormulaCompositionResponse{formulaId, referenceWeightG, lines[FormulaLineResponse{id, phaseId, phase, phaseSortOrder, sortOrder, premixFormulaId, premixCode, premixName, rawMaterialId, commercialName, percentage, isQsp, lot, batch, notIncluded, notIncludedLevel}]} - kind removed.
POST /v1/formulas/{formulaId}/lines, body AddFormulaLineRequest{afterLineId, phase, premixFormulaId?, notIncluded?, notIncludedLevel?} -> 201 + Location + AddFormulaLineResponse{createdLineId, composition}.
PUT /v1/formulas/{formulaId}/lines/{lineId}, body UpdateFormulaLineRequest{phase, rawMaterialId, premixFormulaId, percentage, isQsp, lot, batchId, notIncluded, notIncludedLevel} (full replacement, null clears) -> 200 + FormulaCompositionResponse.
DELETE /v1/formulas/{formulaId}/lines/{lineId} -> 204, usage record reconciled.
GET /v1/formulas/premix-candidates?q=&siteId= -> 200 + List<PremixCandidateResponse{formulaId, code, name, statusCode, statusLabel, statusColorCode, totalPercentage}>, any status.
GET /v1/formulas/{formulaId}/premix-tree?premixFormulaId=&siteId= -> 200 + PremixTreeResponse{premixFormulaId, totalPercentage, nodes[PremixTreeNodeResponse{path, depth, lineId, share, name, isPremix, inci, pricePerUnit, currencyCode, unitCode, notIncluded, notIncludedLevel}]}, capped at 5 levels, priced at siteId.
GET /v1/formulas/{id}/daughters -> unchanged, now non-empty.
Refusals: 400 ProblemDetail with the message key's text for both references, depth over 5, a cycle, a referenced formula that is not a pre-mix, a missing level on a pre-mix formula, a batch on a pre-mix line, and the isPremix flip; 404 for an unknown formula, line or pre-mix. The exact record component names are reconciled against the mock's types at the branch head before authoring.Backend design
API and backend
One aggregate, one service, one controller surface. The pre-mix line is a composition line, so every write and read below extends the classes MP-690 merged (E13, E14, E15); nothing parallel is created.
DTOs.
- FormulaLineResponse: remove kind; add premixFormulaId (UUID, null unless the line references a pre-mix), premixCode and premixName (String, "" when absent, following the record's existing empty-string convention), notIncluded (Boolean), notIncludedLevel (String code or null). This completes the human work request's backend half together with the mapper constant and the test assertion (E1, E2, E3, E4, E5).
- UpdateFormulaLineRequest and AddFormulaLineRequest: add premixFormulaId, notIncluded, notIncludedLevel as plain nullable components, following the update record's documented full-replacement carve-out (E20); isQsp keeps its @NotNull.
- PremixCandidateResponse (new record): formulaId, code, name, statusCode, statusLabel, statusColorCode, totalPercentage.
- PremixTreeResponse and PremixTreeNodeResponse (new records): the tree for one pre-mix line plus one node per ingredient, each with path, depth, its share in its own pre-mix, name, isPremix, inci, pricePerUnit, currencyCode, unitCode, notIncluded and notIncludedLevel. The component names must be read off the mock's PremixTree, PremixIngredientNode and PremixLinePrice types at the branch head and matched exactly, so the flip changes no hook or component (E24, E26).
MAPPER. FormulaCompositionMapper drops LINE_KIND_RAW_MATERIAL and fills the pre-mix trio from line.getPremixFormula(), keeping the rule that it reads only initialised associations - so the composition query fetch-joins the referenced formula (E4, E32). It stays a static utility; the two new read shapes are mapped by the same class or by a sibling static mapper in mapper/formula, as the area's convention requires.
SERVICE: FormulaCompositionService (@Transactional, readOnly for reads). New private validation, invoked by addLine and updateLine before anything is written, so a refusal leaves the composition untouched exactly as the existing rules do:
1. at most one reference per line (message: the preview's both-references wording);
2. the referenced formula exists (404) and its is_premix is true (400);
3. depth at most 5 over the referenced formula's own pre-mix lines;
4. no cycle: a depth-first walk from the edited formula that accepts the same sub-pre-mix reached by two different paths and refuses only a genuine loop - the behaviour the mock review explicitly demanded;
5. the Not included level is required on a line of a formula whose is_premix is true and must be null otherwise;
6. a pre-mix line carries a typed lot only, never a picked batch.
Each throws BusinessRuleException with a messageKey; the depth, cycle and both-references texts reproduce the preview's wording verbatim (E21) so the user sees the same sentence from the optimistic check and from the server. applyValues applies the two references mutually exclusively - a pre-mix clears the raw material and the batch, a raw material clears the pre-mix - so a mixed edit returns the message rather than hitting the CHECK (E13).
USAGE RECORD, single writer. A private reconcile(formulaId) makes the formula's formula_premix_use rows equal the distinct non-null premixFormulaId values of its lines, reading findByFormula_Id and deleting or inserting the difference (E9). Called at the end of addLine, updateLine, deleteLine and copyComposition. copyLines' explicit column list gains the three new columns, lot and batch staying uncopied (E12), and copyComposition reconciles the duplicate afterwards (E14). Nothing else changes for AC10: the daughters endpoint and the flag-off guard are untouched (E10, E11).
FORMULA SERVICE. applyPremixChange gains the level handling for the flip (E11): on true-to-false, clear notIncludedLevel on that formula's lines; on false-to-true, refuse with its own key while a ticked line has no level. findPremixCandidates(q, siteId) reads formulas with is_premix true and no status filter (E19, E31), ranks them like the raw-material candidate search (E17), and computes each pre-mix's own total as the sum of its filled non-QSP line percentages plus a filled QSP line's derived share - as entered, never rescaled, which is what AC5 relies on.
PRE-MIX TREE. FormulaCompositionService#getPremixTree(formulaId, premixFormulaId, siteId) walks the referenced formula's lines level by level, capped at 5, refusing nothing and simply not walking past the cap; it resolves each leaf's name, INCI, price, currency and unit through RawMaterialSiteDataService at the requested siteId (E18), and multiplies the shares along the path so each node also carries its percentage in the final formula. {formulaId} is the formula being edited and siteId the site it is read at, which is what makes the same pre-mix price differently at another site (AC2, AC3). The service serves figures only: the dash with its reason, the excluded count in the footer and the not-100% warning stay in the one client-side cost calculation that already owns MP-690's exclusion reasons (E26).
CONTROLLERS. FormulaCompositionController gains one GET /premix-tree; FormulaController gains one GET /premix-candidates, placed before the /{id} mapping's resolution order. Both are reads open to any authenticated user, like the composition endpoints around them (E15, E16); neither changes an existing signature apart from the request records above.
ERRORS AND i18n. Every refusal is a BusinessRuleException (400) or ResourceNotFoundException (404) reaching GlobalExceptionHandler as a ProblemDetail; no broad catch. Every new key is added to messages.properties and messages_fr.properties beside the existing exception.formula.line.* family (E29).
TESTS. FormulaCompositionServiceTest: one test per rule, the reconciliation on add, update and delete, and the mutually exclusive references; the kind assertion is removed. FormulaServiceTest: the two flip behaviours and the pre-mix candidate query with a Development pre-mix. A repository H2 IT for the migration and its CHECKs. A Keycloak and Postgres controller IT for the two new endpoints: 401 unauthenticated, parameter validation, and the localized text of each new message. Gate: ./gradlew --no-daemon spotlessCheck and ./gradlew --no-daemon test.Backend changes
| File | Change | Why |
|---|---|---|
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/FormulaLineResponse.java | modify | Remove kind (human work request) and add premixFormulaId, premixCode, premixName, notIncluded and notIncludedLevel - the line shape the accepted mock consumes |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/mapper/formula/FormulaCompositionMapper.java | modify | Drop LINE_KIND_RAW_MATERIAL and fill the pre-mix trio and the Not included pair from the line's initialised associations |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/UpdateFormulaLineRequest.java | modify | Three new plain nullable components so a line can be set to a pre-mix and ticked Not included with its level, following the record's full-replacement carve-out |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/AddFormulaLineRequest.java | modify | Allow a line to be created already holding a pre-mix reference and the Not included pair, as the approved design's POST impact states |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/PremixCandidateResponse.java | create | The pre-mix half of the grid's ingredient search: code, name, status code/label/colour and the pre-mix's own total as entered |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/PremixTreeResponse.java | create | The (+) detail's payload for one pre-mix line, carrying the pre-mix's total and its node list |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/PremixTreeNodeResponse.java | create | One ingredient node: path, depth, share, name, isPremix, INCI, per-site price/currency/unit and its Not included pair |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaCompositionService.java | modify | The six write rules with their messages, the mutually exclusive reference assignment, the single writer of the usage record, and the capped priced tree walk |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaService.java | modify | Pre-mix candidate search over is_premix with no status filter, and the Not included level handling on an isPremix flip |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/controller/formula/FormulaCompositionController.java | modify | One new read: GET /{formulaId}/premix-tree with the pre-mix and the site as query parameters |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/controller/formula/FormulaController.java | modify | One new read: GET /premix-candidates, mirroring the raw-material candidate endpoint's parameters |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaRepository.java | modify | The pre-mix candidate query over is_premix with its status row, if the existing specification cannot serve it unchanged |
backend/applications/cosmoreg-core/src/main/resources/messages.properties | modify | New refusal messages: depth over 5, cycle, both references, referenced formula is not a pre-mix, level required, batch on a pre-mix line, and the isPremix flip refusal - the first three in the preview's exact wording |
backend/applications/cosmoreg-core/src/main/resources/messages_fr.properties | modify | The French text of every new key, as the i18n convention requires both files |
backend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaCompositionServiceTest.java | modify | Drop the kind assertion (human work request) and add one test per new rule plus the usage reconciliation on add, update and delete |
backend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaServiceTest.java | modify | The two isPremix flip behaviours and the candidate search returning a pre-mix in Development |
backend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/controller/formula/FormulaPremixCompositionIT.java | create | Keycloak and Postgres controller IT for the two new endpoints: 401, parameter validation and the localized refusal messages |
Data and database6
Data contract
Database
formula_phase_line gains premix_formula_id (UUID, FK formula(id), ON DELETE RESTRICT, NULL), not_included (BOOLEAN NOT NULL DEFAULT FALSE) and not_included_level (VARCHAR(20) NULL, CHECK IN ('THIS_PREMIX','FINAL_FORMULA'), only when not_included). A line holds at most one of rawmaterial_id and premix_formula_id; neither stays legal. A pre-mix line holds no rawmaterial_batch_id. premix_formula_id <> formula_id. One index, idx_formula_phase_line_premix_formula_id. formula_premix_use (V028) is unchanged and holds one row per distinct (formula, pre-mix) pair referenced by the formula's lines, written only by the composition service.Database design
Database
The schema work is transcription, not design: the DB stage's document on the branch is the normative source and was approved at DB Review; this stage authors the migration it deliberately left out (E25).
FILE. backend/applications/cosmoreg-core/src/main/resources/db/migration/V035__Add_Premix_Reference_And_Not_Included_To_Formula_Phase_Line.sql. V034 is the highest version at the indexed base (E30), so V035 is next free; the builder re-checks the number against main before authoring, because another ticket's migration may have landed meanwhile.
COLUMNS on formula_phase_line (E6, E7).
- premix_formula_id UUID NULL, foreign key to formula(id) with an explicit ON DELETE RESTRICT, matching the table's existing raw-material and batch foreign keys and the guarantee that a pre-mix still used by a line cannot be deleted.
- not_included BOOLEAN NOT NULL DEFAULT FALSE. The DEFAULT is what makes the ALTER safe on populated rows; every existing line reads false, which is the behaviour before this ticket.
- not_included_level VARCHAR(20) NULL, a code set fixed by a CHECK rather than reference data - the design's deliberate, reviewed deviation, approved at the gate.
CHECKS, each named and each carrying its IS NOT NULL guard so it never fires on a legal NULL (the pattern the table already uses, E7).
- chk_formula_phase_line_single_reference: a line never holds both references. 'Neither' stays legal, because MP-690 creates an empty line with no raw material, and no new constraint is added for it - the ticket's reference rule and the work request both say so.
- no direct self-reference: premix_formula_id IS NULL OR premix_formula_id <> formula_id. The indirect cases cannot be a row-level CHECK and stay service rules.
- no picked batch on a pre-mix line: premix_formula_id IS NULL OR rawmaterial_batch_id IS NULL, which is the Lot-is-free-text-only rule for a pre-mix line.
- the level's value set: not_included_level IS NULL OR not_included_level IN ('THIS_PREMIX', 'FINAL_FORMULA') - exactly the two codes the mock exchanges (E22).
- the level needs the flag: not_included_level IS NULL OR not_included.
For each CHECK the file states its allowed, rejected-by-value, rejected-by-NULL and unaffected cases, as the design document does.
INDEX. idx_formula_phase_line_premix_formula_id. The table's existing indexes lead with formula_id, rawmaterial_id and rawmaterial_batch_id, so none serves a lookup by the new reference, which the foreign key's RESTRICT check, the level-by-level price walk and the usage reconciliation all perform.
REUSED UNCHANGED. formula_premix_use (V028) keeps its columns, its two foreign keys, its unique pair and its two indexes (E8); this stage gives it its single writer in the service layer, which is what makes the existing daughters reader and the flag-off guard truthful (E9, E10, E11). formula.is_premix and its partial index are untouched and simply queried by the candidate endpoint (E31). rawmaterial_site keeps serving per-site prices (E18).
NOT IN THE SCHEMA, deliberately. A line's resolved price and its reason, a node's share, the percentage in the final formula, a pre-mix's total, its code, name and INCI: all read-time results. The 5-level depth limit, indirect cycles, 'neither reference on a filled line', 'the referenced formula is a pre-mix' and 'the level is required on a pre-mix formula' span several rows or tables and stay service rules, each with the message the design lists.
ENTITY MAPPING (ddl-auto is validate, so it ships with the migration). FormulaPhaseLine gains premixFormula as a LAZY @ManyToOne on premix_formula_id, notIncluded as a non-null Boolean defaulting to false, and notIncludedLevel as @Enumerated(STRING) with length 20; the new enum NotIncludedLevel {THIS_PREMIX, FINAL_FORMULA} lives in domain/formula. The composition fetch join adds the referenced formula so the mapper never lazy-loads (E32).
VERIFICATION. A repository H2 integration test, following the existing H2 IT naming in repository/formula, asserts each CHECK's four cases, the RESTRICT behaviour and that an existing row reads not_included false after the migration.Database changes
| File | Change | Why |
|---|---|---|
backend/applications/cosmoreg-core/src/main/resources/db/migration/V035__Add_Premix_Reference_And_Not_Included_To_Formula_Phase_Line.sql | create | The migration the approved DB design left to this stage: three columns, one foreign key with ON DELETE RESTRICT, five named CHECKs and one index on formula_phase_line, transcribed from the document on the branch |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/domain/formula/FormulaPhaseLine.java | modify | Three fields for the three columns - premixFormula, notIncluded, notIncludedLevel - because ddl-auto is validate and the entity must match the migration |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/domain/formula/NotIncludedLevel.java | create | The two level codes THIS_PREMIX and FINAL_FORMULA as a Java enum mapped STRING, mirroring the CHECK's value set and the codes the mock exchanges |
backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaPhaseLineRepository.java | modify | Fetch-join the referenced pre-mix formula in the composition query, and add the three new columns to the copyLines insert list so a duplicated formula keeps its pre-mix lines |
backend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaPhaseLinePremixH2IT.java | create | Asserts each new CHECK's four cases, the ON DELETE RESTRICT behaviour and that populated rows read not_included false after the migration |
Files and components27
| Area | File | Change | Why |
|---|---|---|---|
| User interface | frontend/src/domains/formulation/features/formulas/api/formula-premix.ts | modify | Flip every function to its apiClient call, keep the merge of the live raw-material search with the pre-mix candidates, map a 400 ProblemDetail into the existing refused result, and drop every TODO(MP-691) marker |
| User interface | frontend/src/domains/formulation/features/formulas/lib/mock-premix-data.ts | delete | The fixtures and their three preview devices are deleted at this stage, which is what removes the seeded demo lines, the fictional daughters and the add-after-a-demo-line limit |
| User interface | frontend/src/domains/formulation/features/formulas/api/formula-composition.ts | modify | The line mirror and mapping gain premixFormulaId, premixCode, premixName, notIncluded and notIncludedLevel; no discriminator returns |
| User interface | frontend/src/domains/formulation/features/formulas/description/hooks/use-formula-composition.ts | modify | Drop the mock overlay merge so the hook reads the live composition only, keeping its refusal and revert contract |
| User interface | frontend/src/domains/formulation/features/formulas/description/hooks/use-formula-description.ts | modify | Read the live daughters endpoint instead of the mock fixture so the existing impact alert counts real formulas |
| Database | backend/applications/cosmoreg-core/src/main/resources/db/migration/V035__Add_Premix_Reference_And_Not_Included_To_Formula_Phase_Line.sql | create | The migration the approved DB design left to this stage: three columns, one foreign key with ON DELETE RESTRICT, five named CHECKs and one index on formula_phase_line, transcribed from the document on the branch |
| Database | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/domain/formula/FormulaPhaseLine.java | modify | Three fields for the three columns - premixFormula, notIncluded, notIncludedLevel - because ddl-auto is validate and the entity must match the migration |
| Database | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/domain/formula/NotIncludedLevel.java | create | The two level codes THIS_PREMIX and FINAL_FORMULA as a Java enum mapped STRING, mirroring the CHECK's value set and the codes the mock exchanges |
| Database | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaPhaseLineRepository.java | modify | Fetch-join the referenced pre-mix formula in the composition query, and add the three new columns to the copyLines insert list so a duplicated formula keeps its pre-mix lines |
| Database | backend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaPhaseLinePremixH2IT.java | create | Asserts each new CHECK's four cases, the ON DELETE RESTRICT behaviour and that populated rows read not_included false after the migration |
| API and backend | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/FormulaLineResponse.java | modify | Remove kind (human work request) and add premixFormulaId, premixCode, premixName, notIncluded and notIncludedLevel - the line shape the accepted mock consumes |
| API and backend | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/mapper/formula/FormulaCompositionMapper.java | modify | Drop LINE_KIND_RAW_MATERIAL and fill the pre-mix trio and the Not included pair from the line's initialised associations |
| API and backend | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/UpdateFormulaLineRequest.java | modify | Three new plain nullable components so a line can be set to a pre-mix and ticked Not included with its level, following the record's full-replacement carve-out |
| API and backend | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/AddFormulaLineRequest.java | modify | Allow a line to be created already holding a pre-mix reference and the Not included pair, as the approved design's POST impact states |
| API and backend | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/PremixCandidateResponse.java | create | The pre-mix half of the grid's ingredient search: code, name, status code/label/colour and the pre-mix's own total as entered |
| API and backend | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/PremixTreeResponse.java | create | The (+) detail's payload for one pre-mix line, carrying the pre-mix's total and its node list |
| API and backend | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/PremixTreeNodeResponse.java | create | One ingredient node: path, depth, share, name, isPremix, INCI, per-site price/currency/unit and its Not included pair |
| API and backend | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaCompositionService.java | modify | The six write rules with their messages, the mutually exclusive reference assignment, the single writer of the usage record, and the capped priced tree walk |
| API and backend | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaService.java | modify | Pre-mix candidate search over is_premix with no status filter, and the Not included level handling on an isPremix flip |
| API and backend | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/controller/formula/FormulaCompositionController.java | modify | One new read: GET /{formulaId}/premix-tree with the pre-mix and the site as query parameters |
| API and backend | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/controller/formula/FormulaController.java | modify | One new read: GET /premix-candidates, mirroring the raw-material candidate endpoint's parameters |
| API and backend | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaRepository.java | modify | The pre-mix candidate query over is_premix with its status row, if the existing specification cannot serve it unchanged |
| API and backend | backend/applications/cosmoreg-core/src/main/resources/messages.properties | modify | New refusal messages: depth over 5, cycle, both references, referenced formula is not a pre-mix, level required, batch on a pre-mix line, and the isPremix flip refusal - the first three in the preview's exact wording |
| API and backend | backend/applications/cosmoreg-core/src/main/resources/messages_fr.properties | modify | The French text of every new key, as the i18n convention requires both files |
| API and backend | backend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaCompositionServiceTest.java | modify | Drop the kind assertion (human work request) and add one test per new rule plus the usage reconciliation on add, update and delete |
| API and backend | backend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaServiceTest.java | modify | The two isPremix flip behaviours and the candidate search returning a pre-mix in Development |
| API and backend | backend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/controller/formula/FormulaPremixCompositionIT.java | create | Keycloak and Postgres controller IT for the two new endpoints: 401, parameter validation and the localized refusal messages |
Files named by the steps
backend/applications/cosmoreg-core/src/main/resources/db/migration/V035__Add_Premix_Reference_And_Not_Included_To_Formula_Phase_Line.sqlbackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/domain/formula/FormulaPhaseLine.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/domain/formula/NotIncludedLevel.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaPhaseLineRepository.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/FormulaLineResponse.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/mapper/formula/FormulaCompositionMapper.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/UpdateFormulaLineRequest.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/AddFormulaLineRequest.javabackend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaCompositionServiceTest.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaCompositionService.javabackend/applications/cosmoreg-core/src/main/resources/messages.propertiesbackend/applications/cosmoreg-core/src/main/resources/messages_fr.propertiesbackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaService.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/controller/formula/FormulaController.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/PremixCandidateResponse.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaRepository.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/controller/formula/FormulaCompositionController.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/PremixTreeResponse.javabackend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/PremixTreeNodeResponse.javabackend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaServiceTest.javabackend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaPhaseLinePremixH2IT.javabackend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/controller/formula/FormulaPremixCompositionIT.javafrontend/src/domains/formulation/features/formulas/api/formula-premix.tsfrontend/src/domains/formulation/features/formulas/lib/mock-premix-data.tsfrontend/src/domains/formulation/features/formulas/api/formula-composition.tsfrontend/src/domains/formulation/features/formulas/description/hooks/use-formula-composition.tsfrontend/src/domains/formulation/features/formulas/description/hooks/use-formula-description.ts
Components the review expects
- kind: create · path: backend/applications/cosmoreg-core/src/main/resources/db/migration/V035__Add_Premix_Reference_And_Not_Included_To_Formula_Phase_Line.sql
- kind: modify · path: backend/applications/cosmoreg-[redacted].java
- kind: create · path: backend/applications/cosmoreg-[redacted].java
- kind: modify · path: backend/applications/cosmoreg-[redacted].java
- kind: modify · path: backend/applications/cosmoreg-[redacted].java
- kind: modify · path: backend/applications/cosmoreg-[redacted].java
- kind: modify · path: backend/applications/cosmoreg-[redacted].java
- kind: modify · path: backend/applications/cosmoreg-[redacted].java
- kind: create · path: backend/applications/cosmoreg-[redacted].java
- kind: create · path: backend/applications/cosmoreg-[redacted].java
- kind: create · path: backend/applications/cosmoreg-[redacted].java
- kind: modify · path: backend/applications/cosmoreg-[redacted].java
- kind: modify · path: backend/applications/cosmoreg-[redacted].java
- kind: modify · path: backend/applications/cosmoreg-[redacted].java
- kind: modify · path: backend/applications/cosmoreg-[redacted].java
- kind: modify · path: backend/applications/cosmoreg-core/src/main/resources/messages.properties
- kind: modify · path: backend/applications/cosmoreg-core/src/main/resources/messages_fr.properties
- kind: modify · path: backend/applications/cosmoreg-[redacted].java
- kind: modify · path: backend/applications/cosmoreg-[redacted].java
- kind: create · path: backend/applications/cosmoreg-[redacted].java
- kind: create · path: backend/applications/cosmoreg-[redacted].java
- kind: modify · path: frontend/src/domains/formulation/features/formulas/api/formula-premix.ts
- kind: delete · path: frontend/src/domains/formulation/features/formulas/lib/mock-premix-data.ts
- kind: modify · path: frontend/src/domains/formulation/features/formulas/api/formula-composition.ts
- kind: modify · path: frontend/src/domains/formulation/features/formulas/description/hooks/use-formula-composition.ts
- kind: modify · path: frontend/src/domains/formulation/features/formulas/description/hooks/use-formula-description.ts
Tests and review checks24
Review checks
- id: RC1 · check: V035 is the only new migration, its version is free on main, and it matches the approved design document on the branch line for line: three columns on formula_phase_line, one foreign key to formula with an explicit ON DELETE RESTRICT, five named CHECKs each with its IS NOT NULL guard, and one index on premix_formula_id. Nothing beyond the document is added, renamed or dropped, and…
- id: RC2 · check: not_included is NOT NULL with DEFAULT FALSE so the ALTER cannot abort on populated rows, not_included_level is constrained to THIS_PREMIX and FINAL_FORMULA and only when the flag is set, and formula_premix_use keeps its columns, keys, unique pair and indexes untouched. An empty line with neither reference is still accepted.
- id: RC3 · check: The entity matches the migration (ddl-auto is validate): premixFormula as a LAZY association, notIncluded non-null defaulting to false, notIncludedLevel mapped STRING with the two-value enum, and the composition query fetch-joins the referenced formula so the mapper never lazy-loads.
- id: RC4 · check: kind is gone from FormulaLineResponse, LINE_KIND_RAW_MATERIAL is gone from FormulaCompositionMapper, and the service test no longer asserts a line kind. No replacement discriminator, flag, computed value or constant of any name takes its place anywhere under backend/: the reference field is the only answer. The picker-label kind on raw-material candidates and the kind of lib/formu…
- id: RC5 · check: The line response carries premixFormulaId, premixCode, premixName, notIncluded and notIncludedLevel, and the two write requests carry premixFormulaId, notIncluded and notIncludedLevel as plain nullable components. Every new record component name matches the mock's own types at the branch head, so no hook and no component had to be adapted.
- id: RC6 · check: A line write applies the two references mutually exclusively - assigning a pre-mix clears the raw material and any picked batch, assigning a raw material clears the pre-mix - so a mixed edit returns the rule's message rather than a constraint violation, and a line holding both is refused.
- id: RC7 · check: Saving a composition that nests pre-mixes more than 5 levels deep is refused, and a formula that would contain itself directly or through other pre-mixes is refused; a sub-pre-mix legitimately reached twice by different paths is ACCEPTED. Both refusal texts are the preview's wording verbatim, in messages.properties and messages_fr.properties.
- id: RC8 · check: Referencing a formula whose is_premix is false is refused; a pre-mix line accepts a typed lot only and never a picked batch; on a formula that is itself a pre-mix a ticked line cannot be saved without a level, and on any other formula the level must be null. Every new message key exists in both properties files.
- id: RC9 · check: One writer of formula_premix_use: after an add, an update, a delete and a duplicate copy, the formula's rows equal the distinct pre-mix references of its lines. Adding a pre-mix to two formulas makes the daughters endpoint report 2, and removing it from one makes it report 1, with no change to the daughters query or to the flag-off guard.
- id: RC10 · check: copyLines carries premix_formula_id, not_included and not_included_level while still leaving lot and batch uncopied, and the duplicate gets its own usage rows - a duplicated formula's pre-mix lines are not rows with neither reference.
- id: RC11 · check: GET /v1/formulas/premix-candidates returns pre-mix formulas whatever their status, including Development, with code, name, status code/label/colour and the pre-mix's own total as entered, never rescaled; the live raw-material candidate endpoint is untouched and the two lists are merged in the data layer, not replaced.
- id: RC12 · check: GET /v1/formulas/{formulaId}/premix-tree walks the referenced formula to at most 5 levels and serves per node its share, name, whether it is itself a pre-mix, its INCI and its pricePerUnit, currencyCode and unitCode resolved at the requested siteId through the existing per-site service. A 50/50 pre-mix of raw materials at 2.00 and 4.00 EUR/kg at site S yields 3.00 EUR/kg, and the…
- id: RC13 · check: Nothing derived is stored or served as a verdict: no price, reason, tree, percentage, total, code, name or INCI column exists, and the dash with its reason plus the footer's excluded count remain the client-side cost calculation's result from the served price, currency and unit. A USD ingredient, an unpriced ingredient and a per-litre ingredient each produce the dash with the rea…
- id: RC14 · check: Turning isPremix off clears the formula's line levels; turning it on is refused, with its own localized message, while a ticked line has no level. The existing refusal to clear the flag while daughters consume the formula still names the daughter codes.
- id: RC15 · check: lib/mock-premix-data.ts is deleted, api/formula-premix.ts contains no fixture import and no TODO(MP-691) marker, and every one of its functions calls apiClient. A rule refusal still arrives as the existing refused result, so the toast-and-revert path and the one-cell revert are unchanged.
- id: RC16 · check: The three mock-stage limits are gone on the preview: a line can be added after a pre-mix line, no demo pre-mix line is seeded into any formula (a formula's total is its own again), and the daughters alert lists only real formulas.
- id: RC17 · check: No behaviour the PO accepted has changed: a pre-mix line's INCI cell is empty, its Lot takes free text with no batch suggestions, the (+) shows ingredients read-only with their INCI and their percentage in the final formula and opens nested pre-mixes to 5 levels with nothing editable, and a pre-mix totalling 95% is used as entered with its warning.
- id: RC18 · check: lib/composition-math.ts, lib/premix-rules.ts, the single pre-mix predicate, premix-detail-rows.tsx, not-included-level-dialog.tsx, premix-badge.tsx, premix-impact-alert.tsx and the column definitions are not rewritten: the flip changed the data source, not the behaviour. Existing estimateCost exclusion reasons are added to, never renamed.
- id: RC19 · check: The verification gate passes on the branch head: ./gradlew --no-daemon spotlessCheck and ./gradlew --no-daemon test in backend/, then npm run build and npm run lint in frontend/. Each of AC1 to AC11 has at least one test naming it, and the two new endpoints have 401, validation and i18n coverage in a Keycloak and Postgres controller IT.
- id: RC20 · check: Scope discipline: no second composition service, controller family or cost calculation; no new npm or Gradle dependency; no change to how a formula is marked as a pre-mix; no currency or unit conversion; no combined INCI list for the finished product; no all-levels-expanded view; and no change to formula_premix_use's shape, to rawmaterial_site or to the live raw-material search.
Test files
backend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaPhaseLinePremixH2IT.javabackend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaCompositionServiceTest.javabackend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaServiceTest.javabackend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/controller/formula/FormulaPremixCompositionIT.java
Reuse to verify
- area: db · element: formula_phase_line (V032) and the V035 migration · action: extend · why: The one composition line table is extended in place, exactly as the approved design describes. A second line table, a child table for the pre-mix reference, a reference-data table for the level or a stored kind column would each be a rejected alternative reappearing.
- area: db · element: formula_premix_use (V028) · action: reuse · why: Reused with its shape, keys and indexes untouched; MP-691 only gives it its single writer. A second record of which formula uses which pre-mix, or a derivation replacing it, must not appear.
- area: backend · element: FormulaCompositionService, FormulaCompositionController and FormulaCompositionMapper · action: extend · why: Rejected alternative A2: a parallel pre-mix composition service or controller family would be a second writer of one table with its own lock, ordering and audit path. Every new rule, the tree walk and the usage reconciliation belong in the existing composition class…
- area: backend · element: FormulaLineResponse.kind and FormulaCompositionMapper.LINE_KIND_RAW_MATERIAL · action: remove · why: The human work request's backend half. Watch for any replacement discriminator under a different name, in a DTO, a mapper, an enum or a test helper.
- area: backend · element: RawMaterialSiteDataService and POST /v1/raw-materials/site-data · action: reuse · why: Per-site price, currency, unit and INCI already exist and must be reused by the tree walk. A second price resolution path, or any stored price on a line, is rejected alternative A3.
- area: backend · element: GET /v1/formulas/{id}/daughters and the isPremix flag-off guard · action: reuse · why: Unchanged code that starts telling the truth once rows are written; a new endpoint or a new counter for AC10 would duplicate it.
- area: frontend · element: lib/mock-premix-data.ts · action: remove · why: The fixtures and their three preview devices must be deleted, not left behind a flag; any surviving seeded line or fictional daughter fails the ticket's mock-stage limits.
- area: frontend · element: lib/composition-math.ts and lib/premix-rules.ts · action: reuse · why: The one cost calculation and the optimistic preview stay as they are. A second cost calculation, a renamed exclusion reason or a server-side price verdict would each break the ticket's live-analogue rule.
Handoff and bindings22
Backend builder takes over the Backend stage from this plan.
- RAG revision
- 6ff9aee4994f
- Catalog
- ead4cae607ae
- Base SHA (RAG indexed)
- 3830b223cba4
- Feature base
- 3830b223cba4
- Branch head
- 945a5346234e
- Main
- 3830b223cba4
- Main overlay
- none
- Branch overlay
- 8dc7ee4dc40b
- Branch
- claude/mock-MP-691-premix-composition-line
- PR
- #143
- Freshness
- EXACT
- RAG mode / profile
- required
- Stage binding
- a51ef40cb084
- Context manifest
- 62d5d3cf887a
- Policy binding
- 08fde414b576
- Writes
- disabled
- TicketSnapshot schema
- ticket-snapshot/v2
- TicketSnapshot hash
- e5c2c3e4a6cf
- TicketSnapshot size
- 32595 B
- TicketSnapshot fetched
- 2026-10-16T16:18:50Z
- FeatureContract schema
- feature-contract/v2.1
- FeatureContract hash
- ffabe7ec47d7
Evidence and provenance32
- Contract
feature-contract/v2.1·ffabe7ec47d78be300cc79fefc3e49a52e5d9af6a454fb483a2a2083bab3ac71- Aggregate
ffabe7ec47d78be300cc79fefc3e49a52e5d9af6a454fb483a2a2083bab3ac71- RAG
- RAG · EXACT
Cited evidence
| Id | Source | Where | Claim |
|---|---|---|---|
| E1 | catalog | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/FormulaLineResponse.java:28-31 @ 3830b22 | The merged line response record has twelve components - id, phaseId, phase, phaseSortOrder, sortOrder, kind, rawMaterialId, commercialName, percentage, isQsp, lot, batch - and no pre-mix reference and no Not included pair. |
| E2 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/FormulaLineResponse.java:19 @ 3830b22 | The record's own javadoc says kind is always RAW_MATERIAL today, which is why the component carries no information and the work request removes it. |
| E3 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/mapper/formula/FormulaCompositionMapper.java:28-29 @ 3830b22 | LINE_KIND_RAW_MATERIAL is a public constant of the mapper, the single source of the discriminator the work request deletes. |
| E4 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/mapper/formula/FormulaCompositionMapper.java:57-64 @ 3830b22 | toLineResponse builds every line positionally and passes the constant in the kind position; it reads the line's phase and raw material only, so the pre-mix trio is added here. |
| E5 | git-base | backend/applications/cosmoreg-core/src/test/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaCompositionServiceTest.java:255-256 @ 3830b22 | The merged service test asserts that the first line's kind equals RAW_MATERIAL; that one assertion must go with the component. |
| E6 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/domain/formula/FormulaPhaseLine.java:59-99 @ 3830b22 | The line entity maps formula_id, phase, rawMaterial, percentage, isQsp, lot, batch and sortOrder - there is no pre-mix reference, no notIncluded and no level, so three entity fields land with the migration. |
| E7 | git-base | backend/applications/cosmoreg-core/src/main/resources/db/migration/V032__Create_Formula_Phase_And_Line_Tables.sql:103-120 @ 3830b22 | formula_phase_line already expresses pairwise line rules as named CHECKs (batch needs a raw material, lot typed or picked) and the one-QSP-per-formula partial unique index; the approved design adds to this pattern rather than replacing it. |
| E8 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/domain/formula/FormulaPremixUse.java:38-63 @ 3830b22 | The pre-mix usage record exists unchanged since MP-612 with both formula foreign keys and its two indexes; MP-691 needs it written, not redesigned. |
| E9 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaPremixUseRepository.java:16-44 @ 3830b22 | The repository already offers findByPremixFormula_Id, countByPremixFormula_Id and findByFormula_Id, so the reconciliation of a formula's usage rows needs no new query method. |
| E10 | rag | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaService.java:200-213 @ 3830b22 | findDaughters is the only reader of the usage record and maps its rows to formula responses; it needs no change for AC10 - it only needs rows to exist. |
| E11 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaService.java:479-512 @ 3830b22 | applyPremixChange already refuses turning the pre-mix flag off while daughters consume the formula, naming their codes; writing the usage record makes that guard effective and is the place the level handling on an isPremix flip belongs. |
| E12 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaPhaseLineRepository.java:89-103 @ 3830b22 | copyLines is a native INSERT ... SELECT with an explicit twelve-column list; without the three new columns a duplicated formula's pre-mix lines would become rows with neither reference. |
| E13 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaCompositionService.java:456-463 @ 3830b22 | applyValues writes raw material, percentage, QSP flag, lot and batch only; the two references must be applied mutually exclusively here or a patched line would hit the CHECK instead of returning the rule's message. |
| E14 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/service/formula/FormulaCompositionService.java:284-301 @ 3830b22 | copyComposition copies phases then lines inside FormulaService#create's transaction, and every composition write first loads the formula through the locked-status guard - the two places the new rules and the usage reconciliation attach. |
| E15 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/controller/formula/FormulaCompositionController.java:41-57 @ 3830b22 | The composition controller is mapped at /v1/formulas/{formulaId} and already serves the composition and every line write, so the pre-mix tree is one more GET on it rather than a parallel route. |
| E16 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/controller/formula/FormulaController.java:224-233 @ 3830b22 | GET /v1/formulas/{id}/daughters already exists and is reused unchanged; the formula controller is also where a formula-level candidate search belongs. |
| E17 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/controller/rawmaterial/RawMaterialFormulaCandidateController.java:37-66 @ 3830b22 | MP-690 serves the grid's ingredient search as GET /v1/raw-materials/formula-candidates?q=&siteId= and resolves per-site data by POST; the pre-mix candidate endpoint mirrors that shape so the grid merges two ranked lists. |
| E18 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/rawmaterial/RawmaterialSiteDataResponse.java:19-37 @ 3830b22 | Per-site resolution already returns pricePerUnit, currencyCode, unitCode and the aggregated active INCI per raw material, which is exactly what each node of a pre-mix tree needs; no price store is added. |
| E19 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/repository/specification/FormulaSpecification.java:136 @ 3830b22 | The formulas search already filters on the isPremix attribute, so the pre-mix candidate query reuses an existing predicate shape over formula.is_premix. |
| E20 | catalog | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/dto/formula/UpdateFormulaLineRequest.java:41-45 @ 3830b22 | The line update request is a documented full replacement with plain nullable components under the backend clearable-field carve-out, so the three new components follow that style rather than Optional<T>. |
| E21 | branch-overlay | frontend/src/domains/formulation/features/formulas/lib/premix-rules.ts:1-40 @ 945a534 | Branch content: the preview fixes PREMIX_MAX_DEPTH at 5 and the exact depth, cycle and both-references refusal messages, and states that the backend owns these rules and must refuse the write itself at this stage. |
| E22 | branch-overlay | frontend/src/domains/formulation/features/formulas/types/index.ts:286-294 @ 945a534 | Branch content: the level is one of exactly two UPPER_SNAKE codes, THIS_PREMIX and FINAL_FORMULA, and is null on a formula that is not a pre-mix. |
| E23 | branch-overlay | frontend/src/domains/formulation/features/formulas/lib/mock-premix-data.ts:1-40 @ 945a534 | Branch content: the fixtures file declares itself deleted at the backend stage and names its three preview devices - site price slots, demo pre-mix lines seeded into every formula, and a usage record derived from the formula's own lines. |
| E24 | branch-overlay | frontend/src/domains/formulation/features/formulas/api/formula-premix.ts:1-40 @ 945a534 | Branch content: every function of the mock api is backed by the fixtures with a TODO(MP-691) marker naming its endpoint, a rule violation is a first-class refused result rather than a thrown error, and the raw-material half of the ingredient search is already live and must stay untouched. |
| E25 | branch-overlay | docs/superpowers/specs/2026-10-13-MP-691-db-schema.md:1-40 @ 945a534 | Branch content: the approved DB design states that it writes no .sql and that the canonical V035 migration is authored by the Backend Builder after DB Review, on this same branch. |
| E26 | branch-overlay | frontend/src/domains/formulation/features/formulas/lib/composition-math.ts:12-14 @ 945a534 | Branch content: the one cost calculation imports the pre-mix tree, node and line-price types, so the price verdict and the exclusion reasons are computed client-side from the figures the tree serves. |
| E27 | branch-overlay | frontend/src/domains/formulation/features/formulas/description/pages/formula-description-page.tsx:74-101 @ 945a534 | Branch content: the impact alert's count is refreshed from the set of the composition's premixFormulaId values, and the page states that both devices go away once the live endpoint serves the daughters with the formula. |
| E28 | branch-overlay | frontend/src/domains/formulation/features/formulas/CLAUDE.md:527-538 @ 945a534 | Branch content: the grid row asks the pre-mix question through a single predicate over premixFormulaId, and a pre-mix line's affordance is its (+) tree rather than the raw-material panel - the frontend contract the flip must not disturb. |
| E29 | git-base | backend/applications/cosmoreg-core/src/main/resources/messages.properties:1091-1102 @ 3830b22 | The composition's refusal messages live as exception.formula.line.* keys next to exception.formula.premix.in.use, which is where the new depth, cycle, reference and level messages belong. |
| E30 | git-base | backend/applications/cosmoreg-core/src/main/resources/db/migration/V034__Add_Formula_And_Default_Flags_To_Internal_Status.sql:1-8 @ 3830b22 | V034 is the highest-numbered Flyway migration at the indexed base, so V035 is the next free version the design names for this ticket. |
| E31 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/domain/formula/Formula.java:133-136 @ 3830b22 | formula.is_premix exists on the formula entity as a non-null flag, so pre-mix candidates are a query over existing data with no schema change. |
| E32 | git-base | backend/applications/cosmoreg-core/src/main/java/com/chromasoftware/cosmoreg/core/repository/formula/FormulaPhaseLineRepository.java:34-43 @ 3830b22 | findCompositionByFormulaId fetch-joins the phase, raw material, batch and batch status in one pass; the referenced pre-mix formula is added to the same fetch join so mapping never lazy-loads. |
Reuse and anti-duplication
| Area | Element | Action | Justification |
|---|---|---|---|
| db | formula_phase_line (V032), the composition line table (db.table:backend/applications/cosmoreg-core/src/main/resources/db/migration/V032__Create_Formula_Phase_And_Line_Tables.sql#formula_phase_line) | extend | The one place a composition line lives, extended in place exactly as the approved design describes: three columns on a table that already carries the raw-material reference, percentage, QSP flag, lot and batch with pairwise CHECKs. |
| db | V035 migration file | create | No migration for this ticket exists: the DB stage deliberately wrote none and its document names the Backend Builder as the author after DB Review (E25). V034 is the highest version at the indexed base (E30), so nothing to extend - the file is transcription, not design. |
| db | formula_premix_use (V028), the pre-mix usage record | reuse | The record exists with both foreign keys, its unique pair and its indexes; MP-691 needs it written, not redesigned. No column, constraint or index changes and no second table records the same fact. |
| db | not_included_level code set and the NotIncludedLevel enum | create | No existing column or code set carries an inclusion level; the approved design settled it as a VARCHAR constrained by a CHECK rather than reference data, and the mock exchanges exactly two UPPER_SNAKE codes. Reopening that choice here is out of scope. |
| backend | FormulaCompositionService, the one composition write service | extend | Every line write already passes through its locked-formula guard and its pre-checks, and applyValues is the single place a line's fields are written (E13, E14). A parallel pre-mix service (A2) would be a second writer of one table with its own lock, ordering and audit path. |
| backend | FormulaCompositionController and FormulaController | extend | The composition surface is already /v1/formulas/{formulaId} and the formula controller owns formula-level reads including /daughters, so the two new reads are one GET each rather than a new controller. |
| backend | FormulaLineResponse.kind (be.dto:backend/applications/cosmoreg-[redacted].java#FormulaLineResponse) | remove | The work request's backend half, deferred here by the mock contract because no earlier routine may touch backend/. The component exists at the base and its own javadoc says the value is always RAW_MATERIAL. |
| backend | FormulaCompositionMapper.LINE_KIND_RAW_MATERIAL and the test's kind assertion | remove | One constant passed positionally into every line response, and one assertion in the merged service test. Both go with the component; no replacement discriminator of any name is introduced. |
| backend | FormulaLineResponse pre-mix trio and Not included pair | extend | The record already carries the line's reference, percentage, QSP flag, lot and batch with an empty-string convention for absent names, so the new fields are components on it, not a new response family. |
| backend | UpdateFormulaLineRequest and AddFormulaLineRequest (be.dto:backend/applications/cosmoreg-[redacted].java#UpdateFormulaLineRequest) | extend | The documented full-replacement record with plain nullable components under the clearable-field carve-out is the write contract the grid already uses; the three new components follow that style rather than Optional<T> or a parallel request. |
| backend | FormulaPhaseLine entity and its composition fetch join | extend | ddl-auto is validate, so the three fields land with the migration on the existing entity, and the referenced formula joins the existing fetch join rather than being lazy-loaded during mapping. |
| backend | FormulaPhaseLineRepository.copyLines column list | extend | The native INSERT ... SELECT lists its columns explicitly, so a duplicated formula would otherwise get pre-mix lines with neither reference. Lot and batch stay deliberately uncopied. |
| backend | The formula_premix_use writer and reconciliation in the composition service | create | Searched the catalog's be.service and be.repository records and the code for any writer: the table has readers only (E9, E10), which is why the alert reads zero. The write is one private reconciliation on the existing service, not a new service, endpoint or table. |
| backend | GET /v1/formulas/{id}/daughters and the isPremix flag-off guard | reuse | Both already read the usage record and need no change for AC10; they simply start reporting the truth. The flip's level handling attaches to the existing applyPremixChange rather than a new endpoint. |
| backend | PremixCandidateResponse and the premix-candidates read | create | Catalog be.route and be.dto for the grid's search return only the raw-material candidate endpoint and its site-data record (E17), which answers for raw materials at a site and cannot carry a formula's code, status and own total. The query itself reuses the existing is_premix predicate shape (E19, E31). |
| backend | PremixTreeResponse, PremixTreeNodeResponse and the tree walk | create | No route or record serves a formula's ingredients level by level with per-site prices; the composition response is one flat level and the site-data record is per raw material (E1, E18). The walk reuses that per-site resolution for every leaf instead of adding a price store. |
| backend | RawMaterialSiteDataService and POST /v1/raw-materials/site-data | reuse | Per-site price, currency, unit and aggregated active INCI already exist in exactly the shape a tree node needs, so the walk calls it; a second price path or a stored price is rejected alternative A3. |
| backend | exception.formula.line.* message keys | extend | The composition's refusals already live as a keyed family in both properties files; the new keys join it, with the depth, cycle and both-references wording copied from the preview so the sentence does not change between client and server. |
| frontend | api/formula-premix.ts, the mock api pair's entry point | extend | Each function already names the endpoint that replaces it and keeps the live raw-material search untouched inside its merge, so the flip rewrites its bodies and keeps its signatures and its refused-result contract. |
| frontend | lib/mock-premix-data.ts and its three preview devices | remove | The file declares itself deleted at this stage and is read only through the mock api; deleting it is what removes the seeded demo lines, the site slots and the fictional daughters the ticket requires gone. |
| frontend | api/formula-composition.ts line mirror (fe.api-client:frontend/src/domains/formulation/features/formulas/api/formula-composition.ts#getFormulaComposition) | extend | The live response mirror gains the five new fields and loses nothing else; the endpoint, its path and every other mapped field stay, and no discriminator is reintroduced. |
| frontend | lib/composition-math.ts and the single pre-mix predicate | reuse | The one cost calculation keeps producing the price, the dash with its reason and the footer count from the served figures, and the row keeps asking the pre-mix question through one predicate over premixFormulaId. A second calculation or a server-side verdict is forbidden by the ticket's live-analogue rule. |
| frontend | use-formula-description and components/premix-impact-alert.tsx | extend | Only the data source changes: the hook reads the live daughters endpoint instead of the fixture, and the alert component itself is not modified, as the mock plan already recorded. |
Bound to
- RAG revision
- 6ff9aee4994f
- Catalog
- ead4cae607ae
- RAG indexed base
- 3830b223cba4
- Feature base
- 3830b223cba4
- Branch head
- 945a5346234e
- Main
- 3830b223cba4
- Ticket snapshot
- e5c2c3e4a6cf
Contract sections
architecture· 15612 B ·7344def129cb· shownfrontend-plan· 6367 B ·1f1bed7c6389· showndb-plan· 7458 B ·05090e47e046· shownbackend-plan· 15362 B ·375759a13782· shownevidence· 16546 B ·125da81ee661· shownreview-plan· 14930 B ·7b0a0e8215ee· shown
Raw logsRaw
Logs are not loaded on this page. Open the Planner run to read them.