hvp-shared 3.2.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -153,7 +153,14 @@ export interface ApiCreatedSuccessResponse<T> extends ApiSuccessResponseBase<T>
153
153
  */
154
154
  export interface ApiErrorDetail {
155
155
  /**
156
- * Error type (machine-readable)
156
+ * Domain-specific error code (machine-readable)
157
+ * Examples: "COLLABORATOR_NOT_FOUND", "INVALID_ACCESS_CODE", "DUPLICATE_EMAIL"
158
+ * Frontend uses this to react programmatically to specific errors
159
+ * See: ErrorCodes in error-codes.types.ts
160
+ */
161
+ code?: string;
162
+ /**
163
+ * Error type (machine-readable category)
157
164
  * Examples:
158
165
  * - "ValidationError" - Input validation failed
159
166
  * - "BusinessRuleError" - Domain rule violated
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Error Codes - Shared Types
3
+ *
4
+ * These error codes are used by both frontend and backend to handle
5
+ * domain-specific errors programmatically.
6
+ *
7
+ * Design Pattern: Error Codes + Factory Methods (Modern Pattern)
8
+ * - Frontend can react to specific error codes
9
+ * - Backend throws domain errors with these codes
10
+ * - Reduces boilerplate by 90% compared to one-class-per-error
11
+ *
12
+ * See: resources/learning/20260107-error-handling-architecture-complete-analysis.md
13
+ */
14
+ /**
15
+ * HTTP Status Codes for error responses
16
+ */
17
+ export declare enum HttpStatus {
18
+ BAD_REQUEST = 400,
19
+ UNAUTHORIZED = 401,
20
+ FORBIDDEN = 403,
21
+ NOT_FOUND = 404,
22
+ CONFLICT = 409,
23
+ UNPROCESSABLE_ENTITY = 422,
24
+ INTERNAL_SERVER_ERROR = 500
25
+ }
26
+ /**
27
+ * Domain-specific error codes
28
+ * Format: RESOURCE_ERROR_TYPE
29
+ */
30
+ export declare const ErrorCodes: {
31
+ readonly COLLABORATOR_NOT_FOUND: {
32
+ readonly code: "COLLABORATOR_NOT_FOUND";
33
+ readonly status: HttpStatus.NOT_FOUND;
34
+ readonly defaultMessage: "Collaborator not found";
35
+ };
36
+ readonly COLLABORATOR_ALREADY_REGISTERED: {
37
+ readonly code: "COLLABORATOR_ALREADY_REGISTERED";
38
+ readonly status: HttpStatus.CONFLICT;
39
+ readonly defaultMessage: "Collaborator is already registered";
40
+ };
41
+ readonly COLLABORATOR_INACTIVE: {
42
+ readonly code: "COLLABORATOR_INACTIVE";
43
+ readonly status: HttpStatus.FORBIDDEN;
44
+ readonly defaultMessage: "Collaborator is inactive";
45
+ };
46
+ readonly CANNOT_DELETE_ACTIVE_COLLABORATOR: {
47
+ readonly code: "CANNOT_DELETE_ACTIVE_COLLABORATOR";
48
+ readonly status: HttpStatus.CONFLICT;
49
+ readonly defaultMessage: "Cannot delete active collaborator";
50
+ };
51
+ readonly DUPLICATE_COLLABORATOR_CODE: {
52
+ readonly code: "DUPLICATE_COLLABORATOR_CODE";
53
+ readonly status: HttpStatus.CONFLICT;
54
+ readonly defaultMessage: "Collaborator code already exists";
55
+ };
56
+ readonly DUPLICATE_COLLABORATOR_EMAIL: {
57
+ readonly code: "DUPLICATE_COLLABORATOR_EMAIL";
58
+ readonly status: HttpStatus.CONFLICT;
59
+ readonly defaultMessage: "Email is already registered";
60
+ };
61
+ readonly INVALID_ACCESS_CODE: {
62
+ readonly code: "INVALID_ACCESS_CODE";
63
+ readonly status: HttpStatus.UNAUTHORIZED;
64
+ readonly defaultMessage: "Invalid access code";
65
+ };
66
+ readonly ACCESS_CODE_NOT_SET: {
67
+ readonly code: "ACCESS_CODE_NOT_SET";
68
+ readonly status: HttpStatus.UNPROCESSABLE_ENTITY;
69
+ readonly defaultMessage: "Access code not set for collaborator";
70
+ };
71
+ readonly INVALID_FISCAL_DATA: {
72
+ readonly code: "INVALID_FISCAL_DATA";
73
+ readonly status: HttpStatus.BAD_REQUEST;
74
+ readonly defaultMessage: "Invalid fiscal data";
75
+ };
76
+ readonly INVALID_CREDENTIALS: {
77
+ readonly code: "INVALID_CREDENTIALS";
78
+ readonly status: HttpStatus.UNAUTHORIZED;
79
+ readonly defaultMessage: "Invalid credentials";
80
+ };
81
+ readonly TOKEN_EXPIRED: {
82
+ readonly code: "TOKEN_EXPIRED";
83
+ readonly status: HttpStatus.UNAUTHORIZED;
84
+ readonly defaultMessage: "Token has expired";
85
+ };
86
+ readonly TOKEN_INVALID: {
87
+ readonly code: "TOKEN_INVALID";
88
+ readonly status: HttpStatus.UNAUTHORIZED;
89
+ readonly defaultMessage: "Invalid token";
90
+ };
91
+ readonly VALIDATION_ERROR: {
92
+ readonly code: "VALIDATION_ERROR";
93
+ readonly status: HttpStatus.BAD_REQUEST;
94
+ readonly defaultMessage: "Validation error";
95
+ };
96
+ readonly INTERNAL_ERROR: {
97
+ readonly code: "INTERNAL_ERROR";
98
+ readonly status: HttpStatus.INTERNAL_SERVER_ERROR;
99
+ readonly defaultMessage: "Internal server error";
100
+ };
101
+ readonly NOT_FOUND: {
102
+ readonly code: "NOT_FOUND";
103
+ readonly status: HttpStatus.NOT_FOUND;
104
+ readonly defaultMessage: "Resource not found";
105
+ };
106
+ readonly UNAUTHORIZED: {
107
+ readonly code: "UNAUTHORIZED";
108
+ readonly status: HttpStatus.UNAUTHORIZED;
109
+ readonly defaultMessage: "Unauthorized";
110
+ };
111
+ readonly FORBIDDEN: {
112
+ readonly code: "FORBIDDEN";
113
+ readonly status: HttpStatus.FORBIDDEN;
114
+ readonly defaultMessage: "Forbidden";
115
+ };
116
+ };
117
+ /**
118
+ * Error code type (union of all error code strings)
119
+ */
120
+ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]['code'];
121
+ /**
122
+ * Error definition type
123
+ */
124
+ export type ErrorDefinition = (typeof ErrorCodes)[keyof typeof ErrorCodes];
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ /**
3
+ * Error Codes - Shared Types
4
+ *
5
+ * These error codes are used by both frontend and backend to handle
6
+ * domain-specific errors programmatically.
7
+ *
8
+ * Design Pattern: Error Codes + Factory Methods (Modern Pattern)
9
+ * - Frontend can react to specific error codes
10
+ * - Backend throws domain errors with these codes
11
+ * - Reduces boilerplate by 90% compared to one-class-per-error
12
+ *
13
+ * See: resources/learning/20260107-error-handling-architecture-complete-analysis.md
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.ErrorCodes = exports.HttpStatus = void 0;
17
+ /**
18
+ * HTTP Status Codes for error responses
19
+ */
20
+ var HttpStatus;
21
+ (function (HttpStatus) {
22
+ HttpStatus[HttpStatus["BAD_REQUEST"] = 400] = "BAD_REQUEST";
23
+ HttpStatus[HttpStatus["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
24
+ HttpStatus[HttpStatus["FORBIDDEN"] = 403] = "FORBIDDEN";
25
+ HttpStatus[HttpStatus["NOT_FOUND"] = 404] = "NOT_FOUND";
26
+ HttpStatus[HttpStatus["CONFLICT"] = 409] = "CONFLICT";
27
+ HttpStatus[HttpStatus["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
28
+ HttpStatus[HttpStatus["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
29
+ })(HttpStatus || (exports.HttpStatus = HttpStatus = {}));
30
+ /**
31
+ * Domain-specific error codes
32
+ * Format: RESOURCE_ERROR_TYPE
33
+ */
34
+ exports.ErrorCodes = {
35
+ // ====================================
36
+ // Collaborator Errors
37
+ // ====================================
38
+ COLLABORATOR_NOT_FOUND: {
39
+ code: 'COLLABORATOR_NOT_FOUND',
40
+ status: HttpStatus.NOT_FOUND,
41
+ defaultMessage: 'Collaborator not found',
42
+ },
43
+ COLLABORATOR_ALREADY_REGISTERED: {
44
+ code: 'COLLABORATOR_ALREADY_REGISTERED',
45
+ status: HttpStatus.CONFLICT,
46
+ defaultMessage: 'Collaborator is already registered',
47
+ },
48
+ COLLABORATOR_INACTIVE: {
49
+ code: 'COLLABORATOR_INACTIVE',
50
+ status: HttpStatus.FORBIDDEN,
51
+ defaultMessage: 'Collaborator is inactive',
52
+ },
53
+ CANNOT_DELETE_ACTIVE_COLLABORATOR: {
54
+ code: 'CANNOT_DELETE_ACTIVE_COLLABORATOR',
55
+ status: HttpStatus.CONFLICT,
56
+ defaultMessage: 'Cannot delete active collaborator',
57
+ },
58
+ DUPLICATE_COLLABORATOR_CODE: {
59
+ code: 'DUPLICATE_COLLABORATOR_CODE',
60
+ status: HttpStatus.CONFLICT,
61
+ defaultMessage: 'Collaborator code already exists',
62
+ },
63
+ DUPLICATE_COLLABORATOR_EMAIL: {
64
+ code: 'DUPLICATE_COLLABORATOR_EMAIL',
65
+ status: HttpStatus.CONFLICT,
66
+ defaultMessage: 'Email is already registered',
67
+ },
68
+ INVALID_ACCESS_CODE: {
69
+ code: 'INVALID_ACCESS_CODE',
70
+ status: HttpStatus.UNAUTHORIZED,
71
+ defaultMessage: 'Invalid access code',
72
+ },
73
+ ACCESS_CODE_NOT_SET: {
74
+ code: 'ACCESS_CODE_NOT_SET',
75
+ status: HttpStatus.UNPROCESSABLE_ENTITY,
76
+ defaultMessage: 'Access code not set for collaborator',
77
+ },
78
+ INVALID_FISCAL_DATA: {
79
+ code: 'INVALID_FISCAL_DATA',
80
+ status: HttpStatus.BAD_REQUEST,
81
+ defaultMessage: 'Invalid fiscal data',
82
+ },
83
+ // ====================================
84
+ // Authentication Errors
85
+ // ====================================
86
+ INVALID_CREDENTIALS: {
87
+ code: 'INVALID_CREDENTIALS',
88
+ status: HttpStatus.UNAUTHORIZED,
89
+ defaultMessage: 'Invalid credentials',
90
+ },
91
+ TOKEN_EXPIRED: {
92
+ code: 'TOKEN_EXPIRED',
93
+ status: HttpStatus.UNAUTHORIZED,
94
+ defaultMessage: 'Token has expired',
95
+ },
96
+ TOKEN_INVALID: {
97
+ code: 'TOKEN_INVALID',
98
+ status: HttpStatus.UNAUTHORIZED,
99
+ defaultMessage: 'Invalid token',
100
+ },
101
+ // ====================================
102
+ // Validation Errors
103
+ // ====================================
104
+ VALIDATION_ERROR: {
105
+ code: 'VALIDATION_ERROR',
106
+ status: HttpStatus.BAD_REQUEST,
107
+ defaultMessage: 'Validation error',
108
+ },
109
+ // ====================================
110
+ // Generic Errors
111
+ // ====================================
112
+ INTERNAL_ERROR: {
113
+ code: 'INTERNAL_ERROR',
114
+ status: HttpStatus.INTERNAL_SERVER_ERROR,
115
+ defaultMessage: 'Internal server error',
116
+ },
117
+ NOT_FOUND: {
118
+ code: 'NOT_FOUND',
119
+ status: HttpStatus.NOT_FOUND,
120
+ defaultMessage: 'Resource not found',
121
+ },
122
+ UNAUTHORIZED: {
123
+ code: 'UNAUTHORIZED',
124
+ status: HttpStatus.UNAUTHORIZED,
125
+ defaultMessage: 'Unauthorized',
126
+ },
127
+ FORBIDDEN: {
128
+ code: 'FORBIDDEN',
129
+ status: HttpStatus.FORBIDDEN,
130
+ defaultMessage: 'Forbidden',
131
+ },
132
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const error_codes_types_1 = require("./error-codes.types");
4
+ describe('ErrorCodes', () => {
5
+ describe('Structure', () => {
6
+ it('should have code, status, and defaultMessage for each error', () => {
7
+ Object.values(error_codes_types_1.ErrorCodes).forEach((errorDef) => {
8
+ expect(errorDef).toHaveProperty('code');
9
+ expect(errorDef).toHaveProperty('status');
10
+ expect(errorDef).toHaveProperty('defaultMessage');
11
+ expect(typeof errorDef.code).toBe('string');
12
+ expect(typeof errorDef.status).toBe('number');
13
+ expect(typeof errorDef.defaultMessage).toBe('string');
14
+ });
15
+ });
16
+ it('should have unique error codes', () => {
17
+ const codes = Object.values(error_codes_types_1.ErrorCodes).map(e => e.code);
18
+ const uniqueCodes = new Set(codes);
19
+ expect(codes.length).toBe(uniqueCodes.size);
20
+ });
21
+ it('should have valid HTTP status codes', () => {
22
+ Object.values(error_codes_types_1.ErrorCodes).forEach((errorDef) => {
23
+ expect(errorDef.status).toBeGreaterThanOrEqual(400);
24
+ expect(errorDef.status).toBeLessThan(600);
25
+ });
26
+ });
27
+ });
28
+ describe('Collaborator Errors', () => {
29
+ it('should have COLLABORATOR_NOT_FOUND with 404 status', () => {
30
+ expect(error_codes_types_1.ErrorCodes.COLLABORATOR_NOT_FOUND.code).toBe('COLLABORATOR_NOT_FOUND');
31
+ expect(error_codes_types_1.ErrorCodes.COLLABORATOR_NOT_FOUND.status).toBe(error_codes_types_1.HttpStatus.NOT_FOUND);
32
+ expect(error_codes_types_1.ErrorCodes.COLLABORATOR_NOT_FOUND.defaultMessage).toBeTruthy();
33
+ });
34
+ it('should have COLLABORATOR_ALREADY_REGISTERED with 409 status', () => {
35
+ expect(error_codes_types_1.ErrorCodes.COLLABORATOR_ALREADY_REGISTERED.code).toBe('COLLABORATOR_ALREADY_REGISTERED');
36
+ expect(error_codes_types_1.ErrorCodes.COLLABORATOR_ALREADY_REGISTERED.status).toBe(error_codes_types_1.HttpStatus.CONFLICT);
37
+ });
38
+ it('should have INVALID_ACCESS_CODE with 401 status', () => {
39
+ expect(error_codes_types_1.ErrorCodes.INVALID_ACCESS_CODE.code).toBe('INVALID_ACCESS_CODE');
40
+ expect(error_codes_types_1.ErrorCodes.INVALID_ACCESS_CODE.status).toBe(error_codes_types_1.HttpStatus.UNAUTHORIZED);
41
+ });
42
+ it('should have DUPLICATE_COLLABORATOR_CODE with 409 status', () => {
43
+ expect(error_codes_types_1.ErrorCodes.DUPLICATE_COLLABORATOR_CODE.code).toBe('DUPLICATE_COLLABORATOR_CODE');
44
+ expect(error_codes_types_1.ErrorCodes.DUPLICATE_COLLABORATOR_CODE.status).toBe(error_codes_types_1.HttpStatus.CONFLICT);
45
+ });
46
+ it('should have DUPLICATE_COLLABORATOR_EMAIL with 409 status', () => {
47
+ expect(error_codes_types_1.ErrorCodes.DUPLICATE_COLLABORATOR_EMAIL.code).toBe('DUPLICATE_COLLABORATOR_EMAIL');
48
+ expect(error_codes_types_1.ErrorCodes.DUPLICATE_COLLABORATOR_EMAIL.status).toBe(error_codes_types_1.HttpStatus.CONFLICT);
49
+ });
50
+ it('should have INVALID_FISCAL_DATA with 400 status', () => {
51
+ expect(error_codes_types_1.ErrorCodes.INVALID_FISCAL_DATA.code).toBe('INVALID_FISCAL_DATA');
52
+ expect(error_codes_types_1.ErrorCodes.INVALID_FISCAL_DATA.status).toBe(error_codes_types_1.HttpStatus.BAD_REQUEST);
53
+ });
54
+ });
55
+ describe('Authentication Errors', () => {
56
+ it('should have INVALID_CREDENTIALS with 401 status', () => {
57
+ expect(error_codes_types_1.ErrorCodes.INVALID_CREDENTIALS.code).toBe('INVALID_CREDENTIALS');
58
+ expect(error_codes_types_1.ErrorCodes.INVALID_CREDENTIALS.status).toBe(error_codes_types_1.HttpStatus.UNAUTHORIZED);
59
+ });
60
+ it('should have TOKEN_EXPIRED with 401 status', () => {
61
+ expect(error_codes_types_1.ErrorCodes.TOKEN_EXPIRED.code).toBe('TOKEN_EXPIRED');
62
+ expect(error_codes_types_1.ErrorCodes.TOKEN_EXPIRED.status).toBe(error_codes_types_1.HttpStatus.UNAUTHORIZED);
63
+ });
64
+ });
65
+ describe('Generic Errors', () => {
66
+ it('should have NOT_FOUND with 404 status', () => {
67
+ expect(error_codes_types_1.ErrorCodes.NOT_FOUND.code).toBe('NOT_FOUND');
68
+ expect(error_codes_types_1.ErrorCodes.NOT_FOUND.status).toBe(error_codes_types_1.HttpStatus.NOT_FOUND);
69
+ });
70
+ it('should have INTERNAL_ERROR with 500 status', () => {
71
+ expect(error_codes_types_1.ErrorCodes.INTERNAL_ERROR.code).toBe('INTERNAL_ERROR');
72
+ expect(error_codes_types_1.ErrorCodes.INTERNAL_ERROR.status).toBe(error_codes_types_1.HttpStatus.INTERNAL_SERVER_ERROR);
73
+ });
74
+ });
75
+ });
76
+ describe('ErrorCode Type', () => {
77
+ it('should allow valid error code strings', () => {
78
+ const validCode = 'COLLABORATOR_NOT_FOUND';
79
+ expect(validCode).toBe('COLLABORATOR_NOT_FOUND');
80
+ });
81
+ it('should be a union of all error code strings', () => {
82
+ // Type test - this compiles if ErrorCode includes these
83
+ const codes = [
84
+ 'COLLABORATOR_NOT_FOUND',
85
+ 'INVALID_ACCESS_CODE',
86
+ 'DUPLICATE_COLLABORATOR_EMAIL',
87
+ 'INVALID_CREDENTIALS',
88
+ 'NOT_FOUND',
89
+ ];
90
+ expect(codes.length).toBe(5);
91
+ });
92
+ });
93
+ describe('ErrorDefinition Type', () => {
94
+ it('should match the structure of error definitions', () => {
95
+ const errorDef = {
96
+ code: 'COLLABORATOR_NOT_FOUND',
97
+ status: 404,
98
+ defaultMessage: 'Collaborator not found',
99
+ };
100
+ expect(errorDef.code).toBe('COLLABORATOR_NOT_FOUND');
101
+ expect(errorDef.status).toBe(404);
102
+ expect(errorDef.defaultMessage).toBe('Collaborator not found');
103
+ });
104
+ });
105
+ describe('HttpStatus Enum', () => {
106
+ it('should have correct HTTP status code values', () => {
107
+ expect(error_codes_types_1.HttpStatus.BAD_REQUEST).toBe(400);
108
+ expect(error_codes_types_1.HttpStatus.UNAUTHORIZED).toBe(401);
109
+ expect(error_codes_types_1.HttpStatus.FORBIDDEN).toBe(403);
110
+ expect(error_codes_types_1.HttpStatus.NOT_FOUND).toBe(404);
111
+ expect(error_codes_types_1.HttpStatus.CONFLICT).toBe(409);
112
+ expect(error_codes_types_1.HttpStatus.UNPROCESSABLE_ENTITY).toBe(422);
113
+ expect(error_codes_types_1.HttpStatus.INTERNAL_SERVER_ERROR).toBe(500);
114
+ });
115
+ });
116
+ describe('Usage Examples', () => {
117
+ it('should be usable for error handling', () => {
118
+ // Simulate backend usage
119
+ const errorDefinition = error_codes_types_1.ErrorCodes.COLLABORATOR_NOT_FOUND;
120
+ const response = {
121
+ ok: false,
122
+ status_code: errorDefinition.status,
123
+ error: {
124
+ code: errorDefinition.code,
125
+ message: errorDefinition.defaultMessage,
126
+ },
127
+ };
128
+ expect(response.status_code).toBe(404);
129
+ expect(response.error.code).toBe('COLLABORATOR_NOT_FOUND');
130
+ });
131
+ it('should be usable for frontend error detection', () => {
132
+ // Simulate frontend usage
133
+ const apiResponse = {
134
+ ok: false,
135
+ error: {
136
+ code: 'COLLABORATOR_NOT_FOUND',
137
+ },
138
+ };
139
+ if (apiResponse.error.code === error_codes_types_1.ErrorCodes.COLLABORATOR_NOT_FOUND.code) {
140
+ // Frontend can react specifically to this error
141
+ expect(true).toBe(true);
142
+ }
143
+ });
144
+ });
@@ -5,3 +5,4 @@
5
5
  export * from './api-response.types';
6
6
  export * from './company-settings.types';
7
7
  export * from './collaborator-fiscal.types';
8
+ export * from './error-codes.types';
@@ -21,3 +21,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
21
21
  __exportStar(require("./api-response.types"), exports);
22
22
  __exportStar(require("./company-settings.types"), exports);
23
23
  __exportStar(require("./collaborator-fiscal.types"), exports);
24
+ __exportStar(require("./error-codes.types"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hvp-shared",
3
- "version": "3.2.1",
3
+ "version": "3.3.0",
4
4
  "description": "Shared types and utilities for HVP backend and frontend",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",