diff --git a/packages/server/src/modules/Items/Item.controller.ts b/packages/server/src/modules/Items/Item.controller.ts index 0bf559cd6..5f174f766 100644 --- a/packages/server/src/modules/Items/Item.controller.ts +++ b/packages/server/src/modules/Items/Item.controller.ts @@ -34,6 +34,7 @@ import { BulkDeleteItemsDto, ValidateBulkDeleteItemsResponseDto, } from './dtos/BulkDeleteItems.dto'; +import { ItemApiErrorResponseDto } from './dtos/ItemErrorResponse.dto'; @Controller('/items') @ApiTags('Items') @@ -45,6 +46,7 @@ import { @ApiExtraModels(ItemEstimatesResponseDto) @ApiExtraModels(ItemReceiptsResponseDto) @ApiExtraModels(ValidateBulkDeleteItemsResponseDto) +@ApiExtraModels(ItemApiErrorResponseDto) @ApiCommonHeaders() export class ItemsController extends TenantController { constructor(private readonly itemsApplication: ItemsApplicationService) { @@ -147,6 +149,13 @@ export class ItemsController extends TenantController { status: 200, description: 'The item has been successfully updated.', }) + @ApiResponse({ + status: 400, + description: 'Validation error. Possible error types: ITEM_NAME_EXISTS, INVENTORY_ACCOUNT_CANNOT_MODIFIED, TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS, etc.', + schema: { + $ref: getSchemaPath(ItemApiErrorResponseDto), + }, + }) @ApiResponse({ status: 404, description: 'The item not found.' }) @ApiParam({ name: 'id', @@ -204,6 +213,13 @@ export class ItemsController extends TenantController { status: 200, description: 'The item has been successfully created.', }) + @ApiResponse({ + status: 400, + description: 'Validation error. Possible error types: ITEM_NAME_EXISTS, ITEM_CATEOGRY_NOT_FOUND, COST_ACCOUNT_NOT_COGS, SELL_ACCOUNT_NOT_INCOME, INVENTORY_ACCOUNT_NOT_INVENTORY, INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM, COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM, etc.', + schema: { + $ref: getSchemaPath(ItemApiErrorResponseDto), + }, + }) // @UsePipes(new ZodValidationPipe(createItemSchema)) async createItem( @Body() createItemDto: CreateItemDto, @@ -219,6 +235,13 @@ export class ItemsController extends TenantController { status: 200, description: 'The item has been successfully deleted.', }) + @ApiResponse({ + status: 400, + description: 'Cannot delete item. Possible error types: ITEM_HAS_ASSOCIATED_TRANSACTINS, ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT, etc.', + schema: { + $ref: getSchemaPath(ItemApiErrorResponseDto), + }, + }) @ApiResponse({ status: 404, description: 'The item not found.' }) @ApiParam({ name: 'id', diff --git a/packages/server/src/modules/Items/dtos/ItemErrorResponse.dto.ts b/packages/server/src/modules/Items/dtos/ItemErrorResponse.dto.ts new file mode 100644 index 000000000..69ed391d7 --- /dev/null +++ b/packages/server/src/modules/Items/dtos/ItemErrorResponse.dto.ts @@ -0,0 +1,112 @@ +import { ApiProperty } from '@nestjs/swagger'; + +/** + * Item API Error Types + * These error types are returned when item operations fail validation + */ +export enum ItemErrorType { + /** Item name already exists in the system */ + ItemNameExists = 'ITEM_NAME_EXISTS', + + /** Item category was not found */ + ItemCategoryNotFound = 'ITEM_CATEOGRY_NOT_FOUND', + + /** Cost account is not a Cost of Goods Sold account */ + CostAccountNotCogs = 'COST_ACCOUNT_NOT_COGS', + + /** Cost account was not found */ + CostAccountNotFound = 'COST_ACCOUNT_NOT_FOUMD', + + /** Sell account was not found */ + SellAccountNotFound = 'SELL_ACCOUNT_NOT_FOUND', + + /** Sell account is not an income account */ + SellAccountNotIncome = 'SELL_ACCOUNT_NOT_INCOME', + + /** Inventory account was not found */ + InventoryAccountNotFound = 'INVENTORY_ACCOUNT_NOT_FOUND', + + /** Account is not an inventory type account */ + InventoryAccountNotInventory = 'INVENTORY_ACCOUNT_NOT_INVENTORY', + + /** Multiple items have associated transactions */ + ItemsHaveAssociatedTransactions = 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', + + /** Item has associated transactions (singular) */ + ItemHasAssociatedTransactions = 'ITEM_HAS_ASSOCIATED_TRANSACTINS', + + /** Item has associated inventory adjustments */ + ItemHasAssociatedInventoryAdjustment = 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', + + /** Cannot change item type to inventory */ + ItemCannotChangeInventoryType = 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE', + + /** Cannot change type when item has transactions */ + TypeCannotChangeWithItemHasTransactions = 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS', + + /** Inventory account cannot be modified */ + InventoryAccountCannotModified = 'INVENTORY_ACCOUNT_CANNOT_MODIFIED', + + /** Purchase tax rate was not found */ + PurchaseTaxRateNotFound = 'PURCHASE_TAX_RATE_NOT_FOUND', + + /** Sell tax rate was not found */ + SellTaxRateNotFound = 'SELL_TAX_RATE_NOT_FOUND', + + /** Income account is required for sellable items */ + IncomeAccountRequiredWithSellableItem = 'INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM', + + /** Cost account is required for purchasable items */ + CostAccountRequiredWithPurchasableItem = 'COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM', + + /** Item not found */ + NotFound = 'NOT_FOUND', + + /** Items not found */ + ItemsNotFound = 'ITEMS_NOT_FOUND', +} + +/** + * Item API Error Response + * Returned when an item operation fails + */ +export class ItemErrorResponseDto { + @ApiProperty({ + description: 'HTTP status code', + example: 400, + }) + statusCode: number; + + @ApiProperty({ + description: 'Error type identifier', + enum: ItemErrorType, + example: ItemErrorType.ItemNameExists, + }) + type: ItemErrorType; + + @ApiProperty({ + description: 'Human-readable error message', + example: 'The item name is already exist.', + required: false, + nullable: true, + }) + message: string | null; + + @ApiProperty({ + description: 'Additional error payload data', + required: false, + nullable: true, + }) + payload: any; +} + +/** + * Item API Error Response Wrapper + */ +export class ItemApiErrorResponseDto { + @ApiProperty({ + description: 'Array of error details', + type: [ItemErrorResponseDto], + }) + errors: ItemErrorResponseDto[]; +} diff --git a/packages/webapp/src/containers/Items/utils.tsx b/packages/webapp/src/containers/Items/utils.tsx index 9e56d3b48..2290fc5e7 100644 --- a/packages/webapp/src/containers/Items/utils.tsx +++ b/packages/webapp/src/containers/Items/utils.tsx @@ -14,6 +14,18 @@ import { useSettingsSelector } from '@/hooks/state'; import { transformItemFormData } from './ItemForm.schema'; import { useWatch } from '@/hooks/utils'; +/** + * Error types for item operations. + */ +export const ItemErrorType = { + ItemNameExists: 'ITEM_NAME_EXISTS', + InventoryAccountCannotModified: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED', + TypeCannotChangeWithItemHasTransactions: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS', + ItemHasAssociatedTransactions: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', + ItemHasAssociatedInventoryAdjustment: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', + ItemHasAssociatedTransactionsPlural: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS', +} as const; + const defaultInitialValues = { active: 1, name: '', @@ -74,7 +86,7 @@ export const transitionItemTypeKeyToLabel = (itemTypeKey) => { // handle delete errors. export const handleDeleteErrors = (errors) => { if ( - errors.find((error) => error.type === 'ITEM_HAS_ASSOCIATED_TRANSACTINS') + errors.find((error) => error.type === ItemErrorType.ItemHasAssociatedTransactions) ) { AppToaster.show({ message: intl.get('the_item_has_associated_transactions'), @@ -84,7 +96,7 @@ export const handleDeleteErrors = (errors) => { if ( errors.find( - (error) => error.type === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', + (error) => error.type === ItemErrorType.ItemHasAssociatedInventoryAdjustment, ) ) { AppToaster.show({ @@ -96,7 +108,7 @@ export const handleDeleteErrors = (errors) => { } if ( errors.find( - (error) => error.type === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS', + (error) => error.type === ItemErrorType.TypeCannotChangeWithItemHasTransactions, ) ) { AppToaster.show({ @@ -107,7 +119,7 @@ export const handleDeleteErrors = (errors) => { }); } if ( - errors.find((error) => error.type === 'ITEM_HAS_ASSOCIATED_TRANSACTIONS') + errors.find((error) => error.type === ItemErrorType.ItemHasAssociatedTransactionsPlural) ) { AppToaster.show({ message: intl.get('item.error.you_could_not_delete_item_has_associated'), @@ -214,10 +226,10 @@ export const transformSubmitRequestErrors = (error) => { } = error; const fields = {}; - if (errors.find((e) => e.type === 'ITEM.NAME.ALREADY.EXISTS')) { + if (errors.find((e) => e.type === ItemErrorType.ItemNameExists)) { fields.name = intl.get('the_name_used_before'); } - if (errors.find((e) => e.type === 'INVENTORY_ACCOUNT_CANNOT_MODIFIED')) { + if (errors.find((e) => e.type === ItemErrorType.InventoryAccountCannotModified)) { AppToaster.show({ message: intl.get('cannot_change_item_inventory_account'), intent: Intent.DANGER, @@ -225,7 +237,7 @@ export const transformSubmitRequestErrors = (error) => { } if ( errors.find( - (e) => e.type === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS', + (e) => e.type === ItemErrorType.TypeCannotChangeWithItemHasTransactions, ) ) { AppToaster.show({