- Plannerstate: Complete
- DB Designerstate: Complete
- Reviewstate: Not started · no automated review recorded for this stage
- Fixstate: Not started
- Human gatestate: Waiting · ready for review (current step)
MP-701
Raw Materials - Multisourcing - Ticket 1 : dedicated tab and group management
Human review gate- Jira status
- DB Review
- Stage
- Human review
- Your move
- Ready for review — non-blocking recommendations are available.
Ready for your review in Jira (DB Review) since 3 days.
- Jira
- Jira confirmed
- Jira read
- Lifecycle activity
- · Agent run
Database design to review
- Migrations
- Design only — no migration included
- API impact
- 10 API impacts described
- Published commit
396b8b28f3fa
API impacts10
Agent-generated evidence — verify before acting
| Endpoint | Change | Reason |
|---|---|---|
GET /v1/raw-materials/{id}/multisourcing-group | Served by a lookup on multisourcing_group_member.rawmaterial_id (unique), joined to multisourcing_group, with memberCount as a COUNT projection. 404 means the material is in no group. | The mock's getGroupForRawMaterial swap marker; the unique key on rawmaterial_id is the at-most-one-group invariant and also the index this read uses (design Q1). |
GET /v1/raw-materials/{id}/multisourcing-group/candidates?search= | Set-equality query over the persisted identity rows (substance_id, percentage NUMERIC(10,6)), plus active = TRUE and not-already-a-member; search is literal containment over the joined code and name. Row cap undecided: Pageable or a stated hard cap. | AC4, enforced server-side on the substance id and never a name. The mock marker asks for a persisted, indexed identity so this is an equality probe, not a per-request re-derivation; the row cap is the one shape the mock leaves open (question 2). |
POST /v1/multisourcing-groups | One transaction: the group row, one member row, and N identity rows aggregated as SUM(percentage_used) GROUP BY substance_id over the founding material's ACTIVE composition. Duplicate-code pre-check on LOWER(code), backed by the unique index. | AC6 and AC-E2 need the refusal in any letter case across active and inactive groups; the identity rows are written once here and never recomputed (design section 5), and SUM hides a NULL percentage so the writer must assert completeness itself. |
PUT /v1/raw-materials/{id}/multisourcing-group | Re-checks the identity rule for the single target and that the target is active, then inserts one member row; the unique key on rawmaterial_id is the ALREADY_IN_GROUP backstop. | The candidate list is a read and can go stale; a retired group must never be rejoinable under its historical code, which is a service rule because active is a legal value of a legal row. |
DELETE /v1/raw-materials/{id}/multisourcing-group | Deletes one multisourcing_group_member row, counts the remaining members and sets active = FALSE when it reaches zero. Idempotent; touches no pricing table. | AC8: the group is never deleted, and no pricing table appears in either statement, which is what makes 'leaving touches no pricing data' structural rather than a promise. |
GET /v1/multisourcing-groups?search=&page=&size=&sort=code,asc,ignorecase | Paged read over every group, active and inactive, ordered by LOWER(code) (the same expression as the unique index), with totalElements from COUNT(*) and memberCount from a COUNT projection; search is literal containment over the space-joined code, name and comment, applied before the page is cut. | AC-E4 to AC-E6. No compatibility, activity or membership filter may be applied here; the emitted SQL must spell LOWER(code) for the index to serve the order. |
PATCH /v1/multisourcing-groups/{groupId} | Updates code, name and comment only, with the duplicate-code check excluding the edited row (id <> :groupId). Name and comment are clearable: plain nullable request components assigned unconditionally, never Optional<T>. | AC-E1 to AC-E3. The identity rows, the member list and active are never touched by this write, which is what makes editing from a catalog of retired groups safe; Jackson maps an omitted key and an explicit null alike to Optional.empty(), which would silently discard a clear. |
DELETE /v1/raw-materials/{id} (existing) | Must delete the raw material's multisourcing_group_member row and deactivate an emptied group before deleting the material; fk_multisourcing_group_member_rawmaterial is ON DELETE RESTRICT, so omitting the step aborts the delete. | RawMaterialService.delete removes children by explicit repository call, so a new table is not in that list; under ON DELETE CASCADE the member row would vanish with no service code running and AC8's deactivation would be skipped silently. |
DELETE /v1/substances/{id} (existing) | Needs a pre-check against multisourcing_group_composition producing a readable in-use refusal; fk_multisourcing_group_composition_substance is ON DELETE RESTRICT. | SubstanceService.delete carries no in-use guard today, so without the pre-check the new foreign key turns that endpoint into a raw constraint error rather than a refusal a user can act on. |
/v1/raw-materials/{rawMaterialId}/multi-sourcing (existing, removed) | All five endpoints are removed with the legacy stack (entity, DTOs, mapper, repository, service, controller) and the RawMaterialService call site, in the same change as DROP TABLE rawmaterial_multisourcing. | AC9's sweep. ddl-auto is validate, so the entity and the drop must land together; the ticket's description of the stack as unused is true of the frontend only, as the controller is mounted and the table is writable today. |
ProvenanceTechnical
- Repository
cosmocomply/monorepo- Published commit
396b8b28f3fa- Push receipt
push-1246- Design document blob
94819597258c- Design artifact
workflow-artifact-e0bd6af273adbee1f23548ff195061aa3bd9f1f3·e0bd6af273ad· 2026-10-14 06:36Z- Accepted by handoff
workflow-handoff-09c8980a9693f80c88f6953d90942988·CLEAN·e374194c6081· 2026-10-14 06:36Z- Validated against contract
feature-contract/v2.1·46f9da7f4aa4- Produced by
db-designer-mp-701-09c8980a
Next action
- Routine
- DB Designer (db-designer)
- Pull request
- #132
- Tests
- 6 passed · 0 failed · 0 not executable · 0 not run
- Review
- Human DB review only; no automated review recorded
Plan and implementation
Plan
Objective. Three new tables (A1): multisourcing_group with code, name, comment, active and audit columns and a unique index on LOWER(code), so a duplicate code is refused in any letter case across active and inactive groups; multisourcing_group_member as a junction with UNIQUE (rawmaterial_id) - the house way to say at most one group per raw material - and with no order column; and multisourcing_group_composition holding the group's identity as one (substance_id, percentage NUMERIC(10,6)) row per substance, written once at creation from the founding material's active composition so the identity survives the group losing every member. The legacy rawmaterial_multisourcing table is dropped in the same V035 migration. Reused unchanged: rawmaterial, the three composition tables, rawmaterial_manufacturer, the AuditableEntity audit columns and the junction pattern. Left out: a column on rawmaterial, a fingerprint hash column, any ordering column, the Flyway file and the entities (Backend stage), and every rule the service owns.
Approach. Target: three new tables in the cosmoreg-core schema plus the removal of the legacy stack's table. Illustrative DDL only - this stage produces a design document; the canonical Flyway file is authored at the Backend stage as V035__Create_Multisourcing_Group_Tables.sql (V034 is the highest version at the base and main added none, E9, E20), and entity and migration land together because ddl-auto is validate (E10). 1. multisourcing_group - the aggregate. id UUID PRIMARY KEY DEFAULT gen_random_uuid(); code VARCHAR(50) NOT NULL - the length is a named business rule (AC-E2: any text up to 50 characters), which is why it is not TEXT (E10); name VARCHAR(150), comment VARCHAR(500) - both nullable: AC-E3's blank values are stored as NULL, which is what the mock's write path does (E19); active BOOLEAN NOT NULL DEFAULT TRUE - declared inside the CREATE TABLE per the convention (E11); set FALSE when the last member leaves, and the row is never deleted (AC8); audit columns created_date/modified_date TIMESTAMPTZ and created_by/modified_by VARCHAR(100), nullable, supplied by AuditableEntity and SystemAwareAuditingEntityListener (E13); CONSTRAINT chk_multisourcing_group_code_trimmed CHECK (code = BTRIM(code) AND code <> '') - makes the whitespace-insensitivity of the uniqueness rule a property of the data rather than of one service method, and keeps the mandatory code from being blank; CREATE UNIQUE INDEX uq_multisourcing_group_code_lower ON multisourcing_group (LOWER(code)) - the AC-E2 and AC6 refusal, case-insensitive across active and inactive rows, and it also serves the catalog's ORDER BY LOWER(code) (the mock's sort=code,asc,ignorecase, E14). No second plain UNIQUE (code): the functional index subsumes it. An index on active is deliberately left to 'consider later' - the catalog does not filter on it and the candidate query is already narrowed by the identity rows. 2. multisourcing_group_member - membership, one row per member. id UUID PRIMARY KEY DEFAULT gen_random_uuid(); group_id UUID NOT NULL, CONSTRAINT fk_multisourcing_group_member_group FOREIGN KEY (group_id) REFERENCES multisourcing_group (id) ON DELETE RESTRICT - a group is never deleted, so a cascade would describe something that must not happen; rawmaterial_id UUID NOT NULL, CONSTRAINT fk_multisourcing_group_member_rawmaterial FOREIGN KEY (rawmaterial_id) REFERENCES rawmaterial (id) ON DELETE CASCADE - a deleted raw material has no membership; audit columns as above; CONSTRAINT uk_multisourcing_group_member_rawmaterial UNIQUE (rawmaterial_id) - this is the 'at most one group per raw material' rule, enforced by the database exactly as rawmaterial_manufacturer enforces its own (E7), and it serves the lookup 'which group does this material belong to'; CREATE INDEX idx_multisourcing_group_member_group ON multisourcing_group_member (group_id) - serves the member count shown on the identity card and in the catalog, and the 'did the last member just leave' check of AC8; No order, priority or rank column: the ticket states this ticket gives a group members, not a ranking, and that nothing here should record it; the sourcing priority arrives with MP-703. 3. multisourcing_group_composition - the group's identity, one row per substance. id UUID PRIMARY KEY DEFAULT gen_random_uuid(); group_id UUID NOT NULL, CONSTRAINT fk_multisourcing_group_composition_group FOREIGN KEY (group_id) REFERENCES multisourcing_group (id) ON DELETE CASCADE; substance_id UUID NOT NULL, CONSTRAINT fk_multisourcing_group_composition_substance FOREIGN KEY (substance_id) REFERENCES substance (id) ON DELETE RESTRICT; percentage NUMERIC(10,6) NOT NULL - the same type and scale as ingredient_composition.percentage_used and allergen_composition.percentage_used (E4, E5) and as the mock's 6-decimal rounding (E15), so two equal compositions always compare equal; NOT NULL is what makes the blank-percentage refusal structural (E6); audit columns as above; CONSTRAINT uk_multisourcing_group_composition_substance UNIQUE (group_id, substance_id) - one row per substance, which is sound because a substance cannot sit in both the ingredient and the allergen list of one composition (stated on the ticket, s17) and the rule merges the two lists (E15); CONSTRAINT chk_multisourcing_group_composition_percentage CHECK (percentage >= 0); CREATE INDEX idx_multisourcing_group_composition_substance ON multisourcing_group_composition (substance_id, percentage) - leading column first, serving the candidate query's lookup by (substance, percentage) pair; Writer, named once: the service inserts these rows in the same transaction as the group, from the founding material's ACTIVE composition (E3). Nothing else writes them and nothing recomputes them; interception of a later composition change is the ticket's excluded scope, and the design document says so where the derivable-data rule of backend/CLAUDE.md asks (E10). Named queries the indexes exist for. Q1 which group does this raw material belong to - multisourcing_group_member by rawmaterial_id (uk_...). Q2 the All groups catalog page - multisourcing_group ORDER BY LOWER(code) with limit and offset plus the total, and the member count per group from multisourcing_group_member (idx_..._group); the search narrows code and name (ILIKE); a trigram index is 'consider later', not core. Q3 join candidates (AC4) - the joiner's N identity pairs are matched against multisourcing_group_composition: the groups whose rows all match and whose own row count is also N, intersected with active = TRUE and with the absence of a member row for this material. The (substance_id, percentage) index serves the match, uk_..._substance guarantees the counts are comparable, and the equality is on the substance id, never on a name (E15). Q4 duplicate code (AC6, AC-E2) - uq_multisourcing_group_code_lower, plus a service pre-check so the user gets the field-level DUPLICATE_GROUP_CODE message rather than a raw constraint error (E14, and the invariant-placement table of E10). Q5 the last member left (AC8) - count members by group_id, then set active = FALSE; the row stays. Entities the Backend stage must land with the migration (ddl-auto validate, E10): MultisourcingGroup, MultisourcingGroupMember and MultisourcingGroupComposition, each extending AuditableEntity with @GeneratedValue(GenerationType.UUID) and SystemAwareAuditingEntityListener, FK joins named exactly as the migration names them (E13). None of the new names contains any AC9 identifier. Legacy sweep. DROP TABLE rawmaterial_multisourcing (no other table references it, E1) together with its entity, DTOs, mapper, repository, service and controller, in the same V035 migration and the same Backend-stage change (ticket s6; inherited Mock decision D7). The design document must state the one awkward consequence: that DROP statement necessarily spells the identifier AC9 greps for, so the gate has to read AC9 as 'no live code references it' with the removing migration as its single allowed occurrence. What the schema deliberately does not express, because the service owns it (E10's invariant table): the no-manufacturer refusal (read from rawmaterial_manufacturer, E7), the no-active-composition and blank-percentage refusals (read from rawmaterial_composition and its entry tables, E3, E6), the compatibility decision itself, and the choice to deactivate a group when its last member leaves. Multi-tenancy: names stay unqualified and no tenant column is added; Flyway runs the migration once per tenant schema, and the legacy drop therefore runs per tenant too (E9). — The evidence decides three things. First, nothing to reuse exists: the catalog's only multisourcing table is a per-material code plus priority order (E1) and the only other group table is a substance classification with neither code nor comment (E2), so group storage is net-new - which is also what the inherited Mock contract's db-plan concluded. Second, membership is a solved shape in this repository: rawmaterial_manufacturer expresses at most one per raw material as a junction with UNIQUE (rawmaterial_id), named FKs and an index (E7), mapped by an AuditableEntity with a UUID PK (E13), and rawmaterial_supplier and rawmaterial_internal_status repeat it; a nullable column on rawmaterial would be the one deviation from that pattern on the domain's busiest table, so A1 keeps the junction. Third, the group's identity must outlive its members: the ticket states that a group's identity is the composition its members share and that a group losing its last member becomes inactive and is never deleted (s5, restated on the branch in E21), which rules out A2, and the mock already stores that identity on the group record rather than deriving it (E16). Between stored rows (A1) and a stored fingerprint (A3) the rows win on auditability and exactness: the comparison the rule names is substance-by-substance at a 6-decimal scale (E15), which NUMERIC(10,6) columns express exactly as the composition tables already do (E4, E5), while a hash hides what a group is and loses the substance FK; backend/CLAUDE.md's derivable-column rule is satisfied by naming the single writer (creation) rather than by refusing to store it (E10). A4 is refused on grain and on AC9 (E1, E12). Group Code uniqueness is case-insensitive across active and inactive groups by AC-E2 and by the product owner's own test (reusing an emptied group's code in lower case must still be refused), which a unique index on LOWER(code) guarantees for every writer, not only for the service; functional and partial unique indexes are already house style (E9). No ordering column appears anywhere, because the ticket says this ticket gives a group members, not a ranking.
Implementation steps
- 1The Multisourcing group schema design document
Database designer
docs/superpowers/specs/2026-10-14-mp-701-db-schema.md
Implementation
- Latest accepted result
- Technically validated · Database ·
- Pull request
- PR #132 (opens in a new tab)
- Verified review commit
e07342b83f73Commit bound and verified by the Operator for the last review. Not necessarily the PR head.- Last attempted commit
e07342b83f73Commit used by the latest agent attempt. Not necessarily the PR head.
Output commits
- Local (attempted) commit — diagnostic only
3812a9930173- Published commit (Broker receipt push-1246)
396b8b28f3fa- Verified PR head
396b8b28f3fa
Test matrix
- 0 Failed
- 0 Not executable here
- 0 Not run
- 6 Passed
Gate reasons are reported by the agent. Agent-generated evidence — verify before acting
| Gate | Status | Command | Reason |
|---|---|---|---|
| AC10: one TODO(MP-701) swap marker per data-layer function | Passed | grep -n 'TODO(MP-701)' frontend/src/domains/raw-materials/api/multisourcing-groups.ts; grep -nE '^export (async )?function' frontend/src/domains/raw-materials/api/multisourcing-groups.ts | 7 exported functions, 7 markers (one each); listAllGroups' marker names page, size and sort=code,asc,ignorecase and updateGroupIdentity's names the code/name/comment body with the 409 DUPLICATE_GROUP_CODE discriminator and the #/code field pointer |
| AC9 (frontend half): five dead-stack identifiers absent | Passed | grep -rniE 'multisourcing-card|MultisourcingCard|useMultisourcing|RawMaterialMultiSourcing|rawmaterial_multisourcing' frontend/src/ CLAUDE.md frontend/CLAUDE.md backend/CLAUDE.md frontend/src/domains/raw-materials/CLAUDE.md | zero hits in frontend source and zero in the CLAUDE.md files; the backend half of the sweep is contract decision D7, deferred to the backend stage |
| contract bounds: frontend-only, no new file, no shared-component change | Passed | git ls-files --others --exclude-standard; git status --porcelain | awk '{print $2}' | grep -E '^(backend/|etl/|infra/|k8s/)|auth\.ts|proxy\.ts|next\.config\.ts|shared/ui/scroll-area|alternative' | zero untracked files (no file created), zero paths under backend/, etl/, infra/, k8s/, zero shared/ changes (shared/ui/scroll-area untouched for its 16 base callers), nothing named alternative_supplier |
| frontend lint (AC11) | Passed | cd /work/frontend && npm run lint | exit 0; 13 problems, 0 errors, 13 warnings, all pre-existing react-hooks/incompatible-library on TanStack useReactTable call sites in files this change does not touch; zero mentions of multisourcing in the lint output |
| frontend production build (AC11) | Passed | cd /work/frontend && npm run build | exit 0; compiled successfully; /raw-materials/[id]/multisourcing present in the route manifest |
| mock-first: no apiClient import for group data (AC10) | Passed | git diff --staged --name-only 5e62a467fa51c068e6386eac56e77943bcfa5a5c | grep -E '\.tsx?$' | xargs -r grep -nE "from ['\"]@/shared/lib/api-client" | no matches in any changed file; the string shared/lib/api-client also appears nowhere in the feature's api, hooks, lib, types or components, including inside the TODO(MP-701) markers |
Review findings and fixer
This gate concerns the database design: no automated review is recorded for it, and the human DB review is the check. Accepting the design establishes no implementation or testing outcome.
Agent work requests
No agent work request for this ticket.
Accepted handoffs and binding
| When | Step | Result | Record |
|---|---|---|---|
| 2026-10-14 06:36Z | design (db) | CLEAN | workflow-handoff-09c8980a9693f80c88f6953d90942988 |
| 2026-10-14 05:33Z | plan (db) | CLEAN | workflow-handoff-cc1fb3c5c9d1356020a98ef98eef88bc |
| 2026-10-11 03:51Z | review (mock) | CLEAN | workflow-handoff-6618083e441b230de021bd55903db8bd |
| 2026-10-11 03:21Z | fix (mock) | CLEAN | workflow-handoff-ea99c0abf10cbd1d8cabd430b2a4589f |
| 2026-10-11 03:11Z | review (mock) | ISSUES | workflow-handoff-7a21c4091b7796f617939f1d10a90ddb |
| 2026-10-11 02:45Z | fix (mock) | CLEAN | workflow-handoff-05a25245508979c570c390079017d6d9 |
| 2026-10-11 02:34Z | review (mock) | ISSUES | workflow-handoff-c1f07b83583251465fd14533600d8513 |
| 2026-10-11 02:06Z | fix (mock) | CLEAN | workflow-handoff-cb58c47684c7bb50fec08b6ad75aebbc |
| 2026-10-11 01:48Z | review (mock) | ISSUES | workflow-handoff-bb8c5e8308c1a3ad3613e992c9cfc43b |
| 2026-10-11 01:13Z | fix (mock) | ISSUES | workflow-handoff-4aeeda61bb1ef4c99a041b721abeb3d1 |
| 2026-10-11 00:50Z | review (mock) | ISSUES | workflow-handoff-ff2c90c3ea495a3f6401477dcd1c5f85 |
| 2026-10-11 00:22Z | fix (mock) | CLEAN | workflow-handoff-21b83c290777dc04583e9a145ac6f594 |
| 2026-10-10 23:51Z | review (mock) | ISSUES | workflow-handoff-5c6d3a2bec1831d57631643401da068b |
| 2026-10-10 23:18Z | fix (mock) | CLEAN | workflow-handoff-d7a52a124cff8511a639c96b02d82ef4 |
| 2026-10-10 23:03Z | review (mock) | ISSUES | workflow-handoff-055b124cfd29d4514f5a17608c1750e5 |
| 2026-10-10 22:25Z | fix (mock) | CLEAN | workflow-handoff-bd3ac3ac52392e5a330dff21a34cb930 |
| 2026-10-10 22:14Z | review (mock) | ISSUES | workflow-handoff-0c9a6fb78d134465a74bb979705d30ea |
| 2026-10-10 19:29Z | review (mock) | ISSUES | workflow-handoff-d4531444580669a535774ccbfa07ecc0 |
| 2026-10-10 15:43Z | build (mock) | CLEAN | workflow-handoff-41597b3678e604f9f4732870609d51b1 |
| 2026-10-10 14:55Z | plan (mock) | CLEAN | workflow-handoff-57d63ffa69c6e38b1641524de03216eb |
- Current binding
workflow-handoff-09c8980a9693f80c88f6953d90942988·e374194c6081- Branch / last attempted commit
claude/mock-MP-701-multisourcing-groups@e07342b83f73
Historical attempts
- Earlier stage Fix: Complete · 2026-10-11 03:21Z
- Earlier stage Review: Complete · clean · 2026-10-11 03:51Z
- History Handoff: Technically validated · Database · 2026-10-14 06:36Z
- History Handoff: Technically validated · Plan · 2026-10-14 05:33Z
- History Handoff: Handoff not accepted · Plan · 2026-10-13 20:17Z
- History Stop record: Technical failure, no work lost · Plan · 2026-10-13 20:17Z
- History Stop record: Technical failure, no work lost · Plan · 2026-10-13 19:48Z
- History Handoff: Technically validated · Review · 2026-10-11 03:51Z
- History Handoff: Technically validated · Review · 2026-10-11 03:21Z
- History Handoff: Corrections requested · Review · 2026-10-11 03:11Z
- History Handoff: Technically validated · Review · 2026-10-11 02:45Z
- History Handoff: Corrections requested · Review · 2026-10-11 02:34Z
- History Handoff: Technically validated · Review · 2026-10-11 02:06Z
- History Handoff: Corrections requested · Review · 2026-10-11 01:48Z
- History Handoff: Corrections requested · Review · 2026-10-11 01:13Z
- History Handoff: Corrections requested · Review · 2026-10-11 00:50Z
- History Handoff: Technically validated · Review · 2026-10-11 00:22Z
- History Handoff: Corrections requested · Review · 2026-10-10 23:51Z
- History Handoff: Technically validated · Review · 2026-10-10 23:18Z
- History Handoff: Corrections requested · Review · 2026-10-10 23:03Z
- History Handoff: Technically validated · Review · 2026-10-10 22:25Z
- History Handoff: Corrections requested · Review · 2026-10-10 22:14Z
Raw records and logs
Raw
Raw records and logs are never loaded on this page. Open them on the technical views: