mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 22:30:31 +00:00
Merge pull request #931 from bigcapitalhq/fix/item-error-handling
fix(items): correct error type handling and add swagger documentation
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
|||||||
BulkDeleteItemsDto,
|
BulkDeleteItemsDto,
|
||||||
ValidateBulkDeleteItemsResponseDto,
|
ValidateBulkDeleteItemsResponseDto,
|
||||||
} from './dtos/BulkDeleteItems.dto';
|
} from './dtos/BulkDeleteItems.dto';
|
||||||
|
import { ItemApiErrorResponseDto } from './dtos/ItemErrorResponse.dto';
|
||||||
|
|
||||||
@Controller('/items')
|
@Controller('/items')
|
||||||
@ApiTags('Items')
|
@ApiTags('Items')
|
||||||
@@ -45,6 +46,7 @@ import {
|
|||||||
@ApiExtraModels(ItemEstimatesResponseDto)
|
@ApiExtraModels(ItemEstimatesResponseDto)
|
||||||
@ApiExtraModels(ItemReceiptsResponseDto)
|
@ApiExtraModels(ItemReceiptsResponseDto)
|
||||||
@ApiExtraModels(ValidateBulkDeleteItemsResponseDto)
|
@ApiExtraModels(ValidateBulkDeleteItemsResponseDto)
|
||||||
|
@ApiExtraModels(ItemApiErrorResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
export class ItemsController extends TenantController {
|
export class ItemsController extends TenantController {
|
||||||
constructor(private readonly itemsApplication: ItemsApplicationService) {
|
constructor(private readonly itemsApplication: ItemsApplicationService) {
|
||||||
@@ -147,6 +149,13 @@ export class ItemsController extends TenantController {
|
|||||||
status: 200,
|
status: 200,
|
||||||
description: 'The item has been successfully updated.',
|
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.' })
|
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@@ -204,6 +213,13 @@ export class ItemsController extends TenantController {
|
|||||||
status: 200,
|
status: 200,
|
||||||
description: 'The item has been successfully created.',
|
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))
|
// @UsePipes(new ZodValidationPipe(createItemSchema))
|
||||||
async createItem(
|
async createItem(
|
||||||
@Body() createItemDto: CreateItemDto,
|
@Body() createItemDto: CreateItemDto,
|
||||||
@@ -219,6 +235,13 @@ export class ItemsController extends TenantController {
|
|||||||
status: 200,
|
status: 200,
|
||||||
description: 'The item has been successfully deleted.',
|
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.' })
|
@ApiResponse({ status: 404, description: 'The item not found.' })
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
|
|||||||
112
packages/server/src/modules/Items/dtos/ItemErrorResponse.dto.ts
Normal file
112
packages/server/src/modules/Items/dtos/ItemErrorResponse.dto.ts
Normal file
@@ -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[];
|
||||||
|
}
|
||||||
@@ -14,6 +14,18 @@ import { useSettingsSelector } from '@/hooks/state';
|
|||||||
import { transformItemFormData } from './ItemForm.schema';
|
import { transformItemFormData } from './ItemForm.schema';
|
||||||
import { useWatch } from '@/hooks/utils';
|
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 = {
|
const defaultInitialValues = {
|
||||||
active: 1,
|
active: 1,
|
||||||
name: '',
|
name: '',
|
||||||
@@ -74,7 +86,7 @@ export const transitionItemTypeKeyToLabel = (itemTypeKey) => {
|
|||||||
// handle delete errors.
|
// handle delete errors.
|
||||||
export const handleDeleteErrors = (errors) => {
|
export const handleDeleteErrors = (errors) => {
|
||||||
if (
|
if (
|
||||||
errors.find((error) => error.type === 'ITEM_HAS_ASSOCIATED_TRANSACTINS')
|
errors.find((error) => error.type === ItemErrorType.ItemHasAssociatedTransactions)
|
||||||
) {
|
) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: intl.get('the_item_has_associated_transactions'),
|
message: intl.get('the_item_has_associated_transactions'),
|
||||||
@@ -84,7 +96,7 @@ export const handleDeleteErrors = (errors) => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
errors.find(
|
errors.find(
|
||||||
(error) => error.type === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
|
(error) => error.type === ItemErrorType.ItemHasAssociatedInventoryAdjustment,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
@@ -96,7 +108,7 @@ export const handleDeleteErrors = (errors) => {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
errors.find(
|
errors.find(
|
||||||
(error) => error.type === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
(error) => error.type === ItemErrorType.TypeCannotChangeWithItemHasTransactions,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
@@ -107,7 +119,7 @@ export const handleDeleteErrors = (errors) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
errors.find((error) => error.type === 'ITEM_HAS_ASSOCIATED_TRANSACTIONS')
|
errors.find((error) => error.type === ItemErrorType.ItemHasAssociatedTransactionsPlural)
|
||||||
) {
|
) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: intl.get('item.error.you_could_not_delete_item_has_associated'),
|
message: intl.get('item.error.you_could_not_delete_item_has_associated'),
|
||||||
@@ -214,10 +226,10 @@ export const transformSubmitRequestErrors = (error) => {
|
|||||||
} = error;
|
} = error;
|
||||||
const fields = {};
|
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');
|
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({
|
AppToaster.show({
|
||||||
message: intl.get('cannot_change_item_inventory_account'),
|
message: intl.get('cannot_change_item_inventory_account'),
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
@@ -225,7 +237,7 @@ export const transformSubmitRequestErrors = (error) => {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
errors.find(
|
errors.find(
|
||||||
(e) => e.type === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
(e) => e.type === ItemErrorType.TypeCannotChangeWithItemHasTransactions,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
|
|||||||
Reference in New Issue
Block a user