summary: "Add three API2 endpoints for AssetType (create, get-one, list) scoped to\ \ a\nproject. AssetType already exists in the xyz schema; this feature adds\nread/write\ \ stored procedures and functions plus API2 runtime code. No new\ntables are introduced.\ \ Uniqueness of Name and Code within a project is\nenforced. Note: the existing\ \ AssetType table has no \"Code\" column \u2014 see risks.\n" target_service: api2 postgres_changes: - kind: alter_table schema: xyz table: AssetType change: "Add a \"Code\" column to xyz.\"AssetType\" to support per-project code\n\ uniqueness required by the spec. The table currently has:\n(Id, InsertedOn, AssetTypeId,\ \ ProjectShardId, CommissioningWorkflowId,\n Name, CreatedBy, LastModifiedOn,\ \ LastModifiedBy).\nAssetType is a pre-May-2026 table (its create file is non-editable),\ \ so\nthe ADD COLUMN goes in a SEPARATE alter file in the same Tables/\ndirectory\ \ (continue from the highest existing 103_..._alter.sql, else\ncreate {next_number}_xyz_assettype_alter.sql\ \ with runOnChange:true).\n" columns_added: - name: Code type: sql: text NULL nullable: true notes: 'COLUMN-CHANGE CONSISTENCY: because we add "Code" to an existing API2-served entity, keep the read/write chain consistent. The general read function fn_GetAssetType / fn_GetAssetTypes and the insert proc usp_InsertAssetType below cover the FULL column set of AssetType, including the new "Code". ' - kind: procedure schema: xyz name: usp_InsertAssetType signature: "usp_InsertAssetType(_projectId UUID, _commissioningWorkflowId UUID,\n\ \ _name TEXT, _code TEXT, _createdBy TEXT)\n" returns: 'Returns the inserted AssetType row (full column set) so the caller avoids a second fetch. Resolves _projectShardId via xyz."fn_GetProjectShardId"(_projectId). ' behavior: 'Validates that no existing (non-deleted) AssetType for this ProjectShardId has the same Name (case-insensitive) or same Code; raises a distinguishable conflict error (SQLSTATE / message the API2 layer maps to 409 Conflict) if either collides. Otherwise inserts a new row with GEN_RANDOM_UUID() AssetTypeId, ProjectShardId, the provided CommissioningWorkflowId, Name, Code, CreatedBy. Includes the standard EXCEPTION block logging to xyz."DbException". ' file_placement: Database/xyz/Procedures/usp_InsertAssetType.sql - kind: function schema: xyz name: fn_GetAssetType signature: fn_GetAssetType(_projectId UUID, _assetTypeId UUID) returns: 'RETURNS TABLE of the full AssetType column set (assetTypeId, projectId, commissioningWorkflowId, name, code, createdBy, insertedOn, lastModifiedOn, lastModifiedBy). Resolves _projectShardId first and filters by ProjectShardId AND AssetTypeId, guaranteeing project isolation (an AssetType belonging to another project returns no rows). ' file_placement: Database/xyz/Functions/fn_GetAssetType.sql - kind: function schema: xyz name: fn_GetAssetTypes signature: fn_GetAssetTypes(_projectId UUID) returns: 'RETURNS TABLE of the full AssetType column set for all AssetTypes in the project. Resolves _projectShardId first and filters by ProjectShardId, ordered by Name. Project-isolated by construction. ' file_placement: Database/xyz/Functions/fn_GetAssetTypes.sql citus_changes: none index_changes: none reporting_changes: none seed_data_changes: none mongo_changes: none api2_changes: - method: POST path: /api/v2/projects/{projectId}/asset-types tag: AssetTypes permissions: - bearerAuth files: routes: src/api/v2/projects/assettypes/assettypes.routes.ts controller: src/api/v2/projects/assettypes/assettypes.controller.ts validator: src/api/v2/projects/assettypes/assettypes.validator.ts service: src/services/assettypes.service.ts request_dto: name: CreateAssetTypeRequest properties: name: type: string required: true code: type: string required: true commissioningWorkflowId: type: string required: true response_dto: name: AssetTypeResponse properties: assetTypeId: type: string projectId: type: string commissioningWorkflowId: type: string name: type: string code: type: string createdBy: type: string insertedOn: type: string lastModifiedOn: type: string nullable: true lastModifiedBy: type: string nullable: true db_calls: - usp_InsertAssetType notes: "usp_InsertAssetType performs the per-project Name/Code uniqueness check\n\ AND the insert atomically, returning the created row \u2014 the controller\ndoes\ \ NOT do a separate pre-fetch or post-fetch. The service maps the\nproc's conflict\ \ error to a 409 Conflict via createApiErrorResponse.\nCOLUMN-CHANGE CONSISTENCY:\ \ since \"Code\" is newly added to AssetType,\nthe ingress DTO (CreateAssetTypeRequest),\ \ egress DTO (AssetTypeResponse),\nthe row-to-DTO mapper, AND all AssetType tests/mocks/fixtures\ \ must\ninclude the \"code\" field so npm test stays green.\n" - method: GET path: /api/v2/projects/{projectId}/asset-types/{assetTypeId} tag: AssetTypes permissions: - bearerAuth files: routes: src/api/v2/projects/assettypes/assettypes.routes.ts controller: src/api/v2/projects/assettypes/assettypes.controller.ts service: src/services/assettypes.service.ts response_dto: name: AssetTypeResponse properties: assetTypeId: type: string projectId: type: string commissioningWorkflowId: type: string name: type: string code: type: string createdBy: type: string insertedOn: type: string lastModifiedOn: type: string nullable: true lastModifiedBy: type: string nullable: true db_calls: - fn_GetAssetType notes: "fn_GetAssetType filters by ProjectShardId AND AssetTypeId in one\nround-trip.\ \ If no row returns, the controller responds 404 NotFound.\nThis naturally returns\ \ 404 for an AssetType that exists in a different\nproject (project-isolation\ \ preserved) \u2014 see open question resolution\nin risks.\n" - method: GET path: /api/v2/projects/{projectId}/asset-types tag: AssetTypes permissions: - bearerAuth files: routes: src/api/v2/projects/assettypes/assettypes.routes.ts controller: src/api/v2/projects/assettypes/assettypes.controller.ts service: src/services/assettypes.service.ts response_dto: name: AssetTypeResponse properties: assetTypeId: type: string projectId: type: string commissioningWorkflowId: type: string name: type: string code: type: string createdBy: type: string insertedOn: type: string lastModifiedOn: type: string nullable: true lastModifiedBy: type: string nullable: true db_calls: - fn_GetAssetTypes notes: "Flat list scoped by project (v1 has no pagination/filter/sort per open\n\ question \u2014 a plain array is sufficient). If a follow-up needs paging,\nswitch\ \ to the PaginationEnvelope pattern then.\n" api1_changes: none inter_service_calls: none new_permissions: none java_frozen_resources: - projects notes_on_frozen: 'The spec touches xyz."Project" (read via fn_GetProjectShardId) which is a dual-API resource. All work lands on API2 + Postgres; API1 hc-project''s ProjectResource is NOT modified. AssetType itself is not a dual-API resource and has no API1 hc-project equivalent. ' risks: - 'The existing xyz."AssetType" table has NO "Code" column (current columns: AssetTypeId, ProjectShardId, CommissioningWorkflowId, Name, CreatedBy, audit cols). The spec requires per-project uniqueness on BOTH name and code, so this plan ADDS a "Code" column. Confirm this is the intended field and that "Code" (not an existing metadata field) is correct before approval. If "code" was meant to reuse an existing field, the alter and uniqueness index should be revised. ' - 'AssetType currently has NO soft-delete columns (IsDeleted etc.), unlike sibling commissioning tables (Asset, CommissioningSystem). The uniqueness check in usp_InsertAssetType therefore treats all rows as live. If future delete support is added, the uniqueness predicate must be revisited. ' - 'AssetType is project-scoped but references CommissioningWorkflowId (NOT NULL). The create endpoint requires the caller to supply a valid commissioningWorkflowId; usp_InsertAssetType should validate it exists in the same project. Confirm the client has this ID available at create time. ' - 'Open question resolution (get-one on cross-project ID): fn_GetAssetType filters by ProjectShardId, so a valid AssetTypeId belonging to another project returns 404 (security-conscious, no cross-project leakage). This is the recommended behavior; confirm at review. ' - 'Unique index on (ProjectShardId, Code) with nullable Code: Postgres treats NULLs as distinct, so multiple NULL codes are allowed. Since the create endpoint requires code, new rows won''t be NULL, but pre-existing rows (backfill) will have NULL Code. Confirm whether a backfill patch is needed. ' out_of_scope: - Update (PUT/PATCH) AssetType endpoint - Delete AssetType endpoint - Tenant-level scoping of AssetTypes - Role-based access control beyond authentication (any authenticated user is authorized) - Pagination / filtering / sorting on the list endpoint (flat list for v1) - Backfilling Code for pre-existing AssetType rows (raised as a risk) testing_plan: 'Postgres: extend IntegrationTest/main.py only if needed to exercise the new procs/functions; primary coverage is the proc/function idempotent deploy via ./build. Verify usp_InsertAssetType rejects duplicate Name and duplicate Code within a project and succeeds across different projects; verify fn_GetAssetType / fn_GetAssetTypes are project-isolated. API2: add unit tests for the controller (request parsing, 409 mapping on duplicate, 404 on cross-project/missing get-one) and e2e tests for all three endpoints under test/e2e/api/v2/projects/assettypes/. Because "Code" is a new column on an API2-served entity, UPDATE existing AssetType-related unit/e2e tests, mocks, and fixtures to include the "code" field so npm test stays green.' _meta: model: claude-opus-4-8 atom_ids: - convention.api2 - rule.architect_rules - convention.api1-hc-project - endpoint.api2.get__api_v2_projects__projectId__users - convention.api1-hc-iam - convention.api1-hc-bpm - endpoint.api2.get__api_v2_projects__projectId__activities_categories - endpoint.api2.delete__api_v2_projects__projectId__activities_categories - endpoint.api2.get__api_v2_projects__projectId__activities_categories__categoryTypeId_ - endpoint.api2.post__api_v2_projects_create-project - endpoint.api2.delete__api_v2_projects__projectId__activities_categories__activityCategoryId_ - endpoint.api2.get__api_v2_projects__projectId__activities - pg.reporting.CalculationMethod - pg.reporting.ProgressOutput - pg.reporting.ProjectCalculationMethod - pg.reporting.ProjectPerformanceSnapshot - pg.reporting.ProjectProgress - pg.staging.DuplicatedMigratedMongoElement - pg.xyz.Project - mongo.hc-project.project - mongo.embedded.Project (iam) - dto.api1-hc-project.Project - endpoint.api1-hc-iam.syncProject - dto.api2.Project spec: title: AssetType CR Endpoints (API2) project_kind: modify user_facing_behavior: "Authenticated users can: (1) Create a new AssetType scoped\ \ to a project \u2014 name and code must each be unique within that project;\ \ (2) Get a single AssetType by ID; (3) List all AssetTypes for a given project.\ \ No update or delete endpoints in this iteration." data_touched: - AssetType - Project api_surface: both non_functional_requirements: - All three endpoints require a valid authentication token (any authenticated user is authorized) - "Name and code uniqueness enforced at the project scope \u2014 duplicate name\ \ or code within the same project must return a clear 409 Conflict error" - "AssetType data is isolated per project \u2014 list and get-one must not return\ \ AssetTypes from other projects" out_of_scope: - Update (PUT/PATCH) endpoint - Delete endpoint - Tenant-level scoping - Role-based access control beyond authentication open_questions: - "What fields does the existing AssetType table/schema have beyond name and code?\ \ (e.g. description, icon, metadata, timestamps) \u2014 architect should inspect\ \ the DB schema" - Should the list endpoint support pagination, filtering, or sorting, or is a flat list sufficient for v1? - Should the project ID be a path parameter (e.g. /projects/:projectId/asset-types) or a query/body parameter? - Should a 404 be returned on get-one if the AssetType exists but belongs to a different project (security-conscious) or only if it truly doesn't exist?