@vulog/aima-unavailability 1.2.7
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.
- package/.eslintrc.js +112 -0
- package/API_SUMMARY.md +36 -0
- package/package.json +29 -0
- package/src/createUnavailability.test.ts +64 -0
- package/src/createUnavailability.ts +31 -0
- package/src/deleteUnavailability.test.ts +71 -0
- package/src/deleteUnavailability.ts +27 -0
- package/src/generateCronExpression.test.ts +112 -0
- package/src/generateCronExpression.ts +61 -0
- package/src/getUnavailabilities.test.ts +123 -0
- package/src/getUnavailabilities.ts +67 -0
- package/src/getUnavailabilitiesByVehicle.test.ts +79 -0
- package/src/getUnavailabilitiesByVehicle.ts +28 -0
- package/src/index.ts +7 -0
- package/src/types.ts +22 -0
- package/src/updateUnavailability.test.ts +78 -0
- package/src/updateUnavailability.ts +39 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +9 -0
- package/vitest.config.ts +9 -0
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
parser: '@typescript-eslint/parser',
|
|
3
|
+
parserOptions: {
|
|
4
|
+
// Indicates the location of the TypeScript configuration file
|
|
5
|
+
project: 'tsconfig.json',
|
|
6
|
+
// Sets the root directory for the TypeScript configuration
|
|
7
|
+
tsconfigRootDir: __dirname,
|
|
8
|
+
// Specifies the version of ECMAScript syntax to be used
|
|
9
|
+
ecmaVersion: 'latest',
|
|
10
|
+
// Indicates the type of source code (script or module)
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
plugins: ['@typescript-eslint', 'prettier', 'import'],
|
|
14
|
+
extends: [
|
|
15
|
+
'airbnb-base',
|
|
16
|
+
'airbnb-typescript/base',
|
|
17
|
+
'plugin:@typescript-eslint/recommended',
|
|
18
|
+
'prettier',
|
|
19
|
+
'plugin:prettier/recommended',
|
|
20
|
+
],
|
|
21
|
+
root: true,
|
|
22
|
+
env: {
|
|
23
|
+
es6: true,
|
|
24
|
+
node: true,
|
|
25
|
+
},
|
|
26
|
+
ignorePatterns: [
|
|
27
|
+
'/dist/**/*', // Ignore built files.
|
|
28
|
+
'.eslintrc.js',
|
|
29
|
+
'**/*.test.ts',
|
|
30
|
+
],
|
|
31
|
+
rules: {
|
|
32
|
+
// Configures the Prettier integration with ESLint
|
|
33
|
+
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
|
34
|
+
// Disables the rule requiring an 'I' prefix for interfaces
|
|
35
|
+
'@typescript-eslint/interface-name-prefix': 'off',
|
|
36
|
+
// Configures the naming conventions for various code constructs
|
|
37
|
+
'@typescript-eslint/naming-convention': [
|
|
38
|
+
// ... various naming convention configurations ...
|
|
39
|
+
'error',
|
|
40
|
+
// Allows any naming format for destructured variables
|
|
41
|
+
{
|
|
42
|
+
selector: 'variable',
|
|
43
|
+
modifiers: ['destructured'],
|
|
44
|
+
format: null,
|
|
45
|
+
},
|
|
46
|
+
// Requires strict camelCase for function names
|
|
47
|
+
{
|
|
48
|
+
selector: 'function',
|
|
49
|
+
format: ['strictCamelCase'],
|
|
50
|
+
},
|
|
51
|
+
// Requires boolean variables to have one of the specified prefixes
|
|
52
|
+
{
|
|
53
|
+
selector: 'variable',
|
|
54
|
+
format: null,
|
|
55
|
+
types: ['boolean'],
|
|
56
|
+
prefix: ['is', 'should', 'has', 'can', 'did', 'will'],
|
|
57
|
+
},
|
|
58
|
+
// Requires enum names to have a strict PascalCase format
|
|
59
|
+
{
|
|
60
|
+
selector: 'enum',
|
|
61
|
+
format: ['StrictPascalCase'],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
'import/extensions': 'off',
|
|
65
|
+
// Disables the rule requiring explicit return types for functions
|
|
66
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
67
|
+
// Disables the rule requiring explicit boundary types for modules
|
|
68
|
+
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
69
|
+
// Disables the rule prohibiting the use of 'any' type
|
|
70
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
71
|
+
// Disables the rule detecting unused variables
|
|
72
|
+
'no-unused-vars': 0,
|
|
73
|
+
// Disables the rule disallowing named exports used as a default export
|
|
74
|
+
'import/no-named-as-default': 0,
|
|
75
|
+
// Configures the order and formatting of import statements
|
|
76
|
+
'import/order': [
|
|
77
|
+
// ... import order configuration ...
|
|
78
|
+
'error',
|
|
79
|
+
{
|
|
80
|
+
// Configure the alphabetization settings
|
|
81
|
+
alphabetize: {
|
|
82
|
+
// Enforce ascending alphabetical order
|
|
83
|
+
order: 'asc',
|
|
84
|
+
// Do not ignore the case while sorting
|
|
85
|
+
caseInsensitive: false,
|
|
86
|
+
},
|
|
87
|
+
// Enforce newlines between different groups and inside groups of imports
|
|
88
|
+
'newlines-between': 'always-and-inside-groups',
|
|
89
|
+
// Warn when there is an import statement that is not part of any group
|
|
90
|
+
warnOnUnassignedImports: true,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
// Configures the rule detecting extraneous dependencies
|
|
94
|
+
'import/no-extraneous-dependencies': [
|
|
95
|
+
// ... extraneous dependencies configuration ...
|
|
96
|
+
'error',
|
|
97
|
+
{
|
|
98
|
+
// Specify the file patterns where devDependencies imports are allowed
|
|
99
|
+
devDependencies: [
|
|
100
|
+
// Allow devDependencies imports in test and spec files
|
|
101
|
+
'**/*.test.{ts,js}',
|
|
102
|
+
'**/*.spec.{ts,js}',
|
|
103
|
+
// Allow devDependencies imports in the 'test' folder
|
|
104
|
+
'./test/**.{ts,js}',
|
|
105
|
+
// Allow devDependencies imports in the 'scripts' folder
|
|
106
|
+
'./scripts/**/*.{ts,js}',
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
'import/prefer-default-export': 'off',
|
|
111
|
+
},
|
|
112
|
+
};
|
package/API_SUMMARY.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Vulog Unavailability API Calls - Summary
|
|
2
|
+
|
|
3
|
+
## Implemented API Endpoints
|
|
4
|
+
|
|
5
|
+
### 1. GET `/boapi/proxy/user/fleets/{fleetId}/scheduledRules/unavailability/vehicles/{vehicleId}`
|
|
6
|
+
Lists all unavailability rules for a specific vehicle. Returns array of `Unavailability` objects. Returns empty array on 404.
|
|
7
|
+
|
|
8
|
+
### 2. GET `/boapi/proxy/user/fleets/{fleetId}/scheduledRules/unavailability/vehicles`
|
|
9
|
+
Lists unavailability rules with date range filtering and pagination. Query parameters: `from` (ISO date), `to` (ISO date), `page` (number). Automatically iterates through pages until no more results.
|
|
10
|
+
|
|
11
|
+
### 3. POST `/boapi/proxy/user/fleets/{fleetId}/scheduledRules/unavailability`
|
|
12
|
+
Creates a new unavailability rule. Request body: `{ cronExpression, duration, maintenanceTitle, vehicleId }`. Returns created `Unavailability` object.
|
|
13
|
+
|
|
14
|
+
### 4. PUT `/boapi/proxy/user/fleets/{fleetId}/scheduledRules/unavailability/{id}`
|
|
15
|
+
Updates an existing unavailability rule. Request body same as POST. Returns updated `Unavailability` object.
|
|
16
|
+
|
|
17
|
+
### 5. DELETE `/boapi/proxy/user/fleets/{fleetId}/scheduledRules/unavailability/{id}`
|
|
18
|
+
Deletes an unavailability rule by ID. Idempotent - returns successfully if already deleted (404).
|
|
19
|
+
|
|
20
|
+
## Utility Function
|
|
21
|
+
|
|
22
|
+
### `generateCronExpression(date: Date | string, timezone?: string): string`
|
|
23
|
+
Generates Quartz cron expression from a date. Supports timezone conversion. Format: `second minute hour day month ? year`.
|
|
24
|
+
|
|
25
|
+
## Type Definitions
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
type Unavailability = {
|
|
29
|
+
id: string;
|
|
30
|
+
cronExpression: string;
|
|
31
|
+
duration: number;
|
|
32
|
+
maintenanceTitle: string;
|
|
33
|
+
vehicleId: string;
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vulog/aima-unavailability",
|
|
3
|
+
"version": "1.2.7",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"module": "dist/index.mjs",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsup",
|
|
9
|
+
"dev": "tsup --watch",
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"test:watch": "vitest",
|
|
12
|
+
"lint": "eslint src/**/* --ext .ts"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"AIMA",
|
|
16
|
+
"VULOG",
|
|
17
|
+
"UNAVAILABILITY"
|
|
18
|
+
],
|
|
19
|
+
"author": "Vulog",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@vulog/aima-client": "1.2.7",
|
|
23
|
+
"@vulog/aima-core": "1.2.7"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"zod": "^3.25.76"
|
|
27
|
+
},
|
|
28
|
+
"description": ""
|
|
29
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, test, vi, expect, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { Client } from '@vulog/aima-client';
|
|
4
|
+
import { createUnavailability } from './createUnavailability';
|
|
5
|
+
import { Unavailability } from './types';
|
|
6
|
+
|
|
7
|
+
describe('createUnavailability', () => {
|
|
8
|
+
const postMock = vi.fn();
|
|
9
|
+
const client = {
|
|
10
|
+
post: postMock,
|
|
11
|
+
clientOptions: {
|
|
12
|
+
fleetId: 'FLEET_ID',
|
|
13
|
+
},
|
|
14
|
+
} as unknown as Client;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should return invalid args for invalid body', async () => {
|
|
21
|
+
const body = {
|
|
22
|
+
cronExpression: '',
|
|
23
|
+
duration: -1,
|
|
24
|
+
maintenanceTitle: '',
|
|
25
|
+
vehicleId: 'INVALID_UUID',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
await expect(createUnavailability(client, body)).rejects.toThrow('Invalid args');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should create unavailability successfully', async () => {
|
|
32
|
+
const body = {
|
|
33
|
+
cronExpression: '0 0 23 * * ? *',
|
|
34
|
+
duration: 60,
|
|
35
|
+
maintenanceTitle: 'GetAround booking 234535',
|
|
36
|
+
vehicleId: '550e8400-e29b-41d4-a716-446655440000',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const mockResponse: Unavailability = {
|
|
40
|
+
id: '550e8400-e29b-41d4-a716-446655440001',
|
|
41
|
+
cronExpression: '0 0 23 * * ? *',
|
|
42
|
+
duration: 60,
|
|
43
|
+
maintenanceTitle: 'GetAround booking 234535',
|
|
44
|
+
vehicleId: '550e8400-e29b-41d4-a716-446655440000',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
postMock.mockResolvedValueOnce({
|
|
48
|
+
data: mockResponse,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const result = await createUnavailability(client, body);
|
|
52
|
+
expect(result).toEqual(mockResponse);
|
|
53
|
+
expect(postMock).toBeCalledWith(
|
|
54
|
+
`/boapi/proxy/user/fleets/FLEET_ID/scheduledRules/unavailability`,
|
|
55
|
+
{
|
|
56
|
+
cronExpression: body.cronExpression,
|
|
57
|
+
duration: body.duration,
|
|
58
|
+
maintenanceTitle: body.maintenanceTitle,
|
|
59
|
+
vehicleId: body.vehicleId,
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Client } from '@vulog/aima-client';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { CreateUnavailabilityBody, Unavailability } from './types';
|
|
5
|
+
|
|
6
|
+
const schema = z.object({
|
|
7
|
+
cronExpression: z.string().min(1),
|
|
8
|
+
duration: z.number().int().positive(),
|
|
9
|
+
maintenanceTitle: z.string().min(1),
|
|
10
|
+
vehicleId: z.string().uuid(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const createUnavailability = async (client: Client, body: CreateUnavailabilityBody): Promise<Unavailability> => {
|
|
14
|
+
const result = schema.safeParse(body);
|
|
15
|
+
if (!result.success) {
|
|
16
|
+
throw new TypeError('Invalid args', {
|
|
17
|
+
cause: result.error.issues,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return client
|
|
21
|
+
.post<Unavailability>(
|
|
22
|
+
`/boapi/proxy/user/fleets/${client.clientOptions.fleetId}/scheduledRules/unavailability`,
|
|
23
|
+
{
|
|
24
|
+
cronExpression: result.data.cronExpression,
|
|
25
|
+
duration: result.data.duration,
|
|
26
|
+
maintenanceTitle: result.data.maintenanceTitle,
|
|
27
|
+
vehicleId: result.data.vehicleId,
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
.then(({ data }) => data);
|
|
31
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, test, vi, expect, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { Client } from '@vulog/aima-client';
|
|
4
|
+
import { deleteUnavailability } from './deleteUnavailability';
|
|
5
|
+
|
|
6
|
+
describe('deleteUnavailability', () => {
|
|
7
|
+
const deleteMock = vi.fn();
|
|
8
|
+
const client = {
|
|
9
|
+
delete: deleteMock,
|
|
10
|
+
clientOptions: {
|
|
11
|
+
fleetId: 'FLEET_ID',
|
|
12
|
+
},
|
|
13
|
+
} as unknown as Client;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should return invalid args for invalid id', async () => {
|
|
20
|
+
const id = 'INVALID_UUID';
|
|
21
|
+
|
|
22
|
+
await expect(deleteUnavailability(client, id)).rejects.toThrow('Invalid args');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should handle 404 gracefully (idempotent)', async () => {
|
|
26
|
+
const id = '550e8400-e29b-41d4-a716-446655440001';
|
|
27
|
+
|
|
28
|
+
deleteMock.mockRejectedValueOnce({
|
|
29
|
+
formattedError: {
|
|
30
|
+
status: 404,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const result = await deleteUnavailability(client, id);
|
|
35
|
+
expect(result).toBeUndefined();
|
|
36
|
+
expect(deleteMock).toBeCalledWith(
|
|
37
|
+
`/boapi/proxy/user/fleets/FLEET_ID/scheduledRules/unavailability/${id}`
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('should delete unavailability successfully', async () => {
|
|
42
|
+
const id = '550e8400-e29b-41d4-a716-446655440001';
|
|
43
|
+
|
|
44
|
+
deleteMock.mockResolvedValueOnce({
|
|
45
|
+
data: undefined,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const result = await deleteUnavailability(client, id);
|
|
49
|
+
expect(result).toBeUndefined();
|
|
50
|
+
expect(deleteMock).toBeCalledWith(
|
|
51
|
+
`/boapi/proxy/user/fleets/FLEET_ID/scheduledRules/unavailability/${id}`
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('should throw error on non-404 errors', async () => {
|
|
56
|
+
const id = '550e8400-e29b-41d4-a716-446655440001';
|
|
57
|
+
|
|
58
|
+
deleteMock.mockRejectedValueOnce({
|
|
59
|
+
formattedError: {
|
|
60
|
+
status: 500,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await expect(deleteUnavailability(client, id)).rejects.toEqual({
|
|
65
|
+
formattedError: {
|
|
66
|
+
status: 500,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Client } from '@vulog/aima-client';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const schema = z.object({
|
|
5
|
+
id: z.string().trim().min(1).uuid(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const deleteUnavailability = async (client: Client, id: string): Promise<void> => {
|
|
9
|
+
const result = schema.safeParse({ id });
|
|
10
|
+
if (!result.success) {
|
|
11
|
+
throw new TypeError('Invalid args', {
|
|
12
|
+
cause: result.error.issues,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return client
|
|
16
|
+
.delete(
|
|
17
|
+
`/boapi/proxy/user/fleets/${client.clientOptions.fleetId}/scheduledRules/unavailability/${result.data.id}`
|
|
18
|
+
)
|
|
19
|
+
.then(() => undefined)
|
|
20
|
+
.catch((error) => {
|
|
21
|
+
// Handle 404 gracefully (idempotent - if already deleted, that's fine)
|
|
22
|
+
if (error.formattedError?.status === 404) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
});
|
|
27
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { generateCronExpression } from './generateCronExpression';
|
|
4
|
+
|
|
5
|
+
describe('generateCronExpression', () => {
|
|
6
|
+
test('should generate cron expression from Date object', () => {
|
|
7
|
+
const date = new Date('2018-08-14T23:00:00Z');
|
|
8
|
+
const cron = generateCronExpression(date);
|
|
9
|
+
|
|
10
|
+
// Note: This will depend on the local timezone where the test runs
|
|
11
|
+
// The date is in UTC, so we expect it to be converted to local time
|
|
12
|
+
// Just verify the format is correct
|
|
13
|
+
expect(cron).toMatch(/^\d+ \d+ \d+ \d+ 8 \? 2018$/);
|
|
14
|
+
expect(cron.split(' ').length).toBe(7);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should generate cron expression from ISO string', () => {
|
|
18
|
+
const dateStr = '2018-08-14T23:00:00Z';
|
|
19
|
+
const cron = generateCronExpression(dateStr);
|
|
20
|
+
|
|
21
|
+
// Just verify the format is correct (date will vary by timezone)
|
|
22
|
+
expect(cron).toMatch(/^\d+ \d+ \d+ \d+ 8 \? 2018$/);
|
|
23
|
+
expect(cron.split(' ').length).toBe(7);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should generate cron expression with specific timezone', () => {
|
|
27
|
+
// Test with Europe/Paris timezone (UTC+2 in summer)
|
|
28
|
+
const date = new Date('2018-08-14T21:00:00Z'); // 21:00 UTC = 23:00 CEST
|
|
29
|
+
const cron = generateCronExpression(date, 'Europe/Paris');
|
|
30
|
+
|
|
31
|
+
// Should be 23:00 in Paris timezone
|
|
32
|
+
expect(cron).toMatch(/^\d+ 0 23 14 8 \? 2018$/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should generate cron expression with America/New_York timezone', () => {
|
|
36
|
+
// Test with America/New_York timezone (UTC-4 in summer)
|
|
37
|
+
const date = new Date('2018-08-14T23:00:00Z'); // 23:00 UTC = 19:00 EDT
|
|
38
|
+
const cron = generateCronExpression(date, 'America/New_York');
|
|
39
|
+
|
|
40
|
+
// Should be 19:00 in New York timezone
|
|
41
|
+
expect(cron).toMatch(/^\d+ 0 19 14 8 \? 2018$/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should handle date with specific time components', () => {
|
|
45
|
+
const date = new Date('2018-08-14T15:30:45Z');
|
|
46
|
+
const cron = generateCronExpression(date, 'UTC');
|
|
47
|
+
|
|
48
|
+
// Should match: 45 30 15 14 8 ? 2018
|
|
49
|
+
expect(cron).toBe('45 30 15 14 8 ? 2018');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('should handle date at midnight', () => {
|
|
53
|
+
const date = new Date('2018-08-14T00:00:00Z');
|
|
54
|
+
const cron = generateCronExpression(date, 'UTC');
|
|
55
|
+
|
|
56
|
+
expect(cron).toBe('0 0 0 14 8 ? 2018');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should handle date at end of day', () => {
|
|
60
|
+
const date = new Date('2018-08-14T23:59:59Z');
|
|
61
|
+
const cron = generateCronExpression(date, 'UTC');
|
|
62
|
+
|
|
63
|
+
expect(cron).toBe('59 59 23 14 8 ? 2018');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should handle different months correctly', () => {
|
|
67
|
+
const date = new Date('2018-12-25T12:00:00Z');
|
|
68
|
+
const cron = generateCronExpression(date, 'UTC');
|
|
69
|
+
|
|
70
|
+
expect(cron).toBe('0 0 12 25 12 ? 2018');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('should handle different years correctly', () => {
|
|
74
|
+
const date = new Date('2024-01-01T00:00:00Z');
|
|
75
|
+
const cron = generateCronExpression(date, 'UTC');
|
|
76
|
+
|
|
77
|
+
expect(cron).toBe('0 0 0 1 1 ? 2024');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('should throw error for invalid date string', () => {
|
|
81
|
+
expect(() => generateCronExpression('invalid-date')).toThrow('Invalid date provided');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('should throw error for invalid Date object', () => {
|
|
85
|
+
const invalidDate = new Date('invalid');
|
|
86
|
+
expect(() => generateCronExpression(invalidDate)).toThrow('Invalid date provided');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should handle timezone conversion correctly for different dates', () => {
|
|
90
|
+
// Test with a date that might have different DST rules
|
|
91
|
+
const date = new Date('2018-01-14T12:00:00Z'); // Winter in Europe/Paris (UTC+1)
|
|
92
|
+
const cron = generateCronExpression(date, 'Europe/Paris');
|
|
93
|
+
|
|
94
|
+
// Should be 13:00 in Paris timezone (UTC+1 in winter)
|
|
95
|
+
expect(cron).toMatch(/^\d+ 0 13 14 1 \? 2018$/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should handle edge case: last day of month', () => {
|
|
99
|
+
const date = new Date('2018-01-31T12:00:00Z');
|
|
100
|
+
const cron = generateCronExpression(date, 'UTC');
|
|
101
|
+
|
|
102
|
+
expect(cron).toBe('0 0 12 31 1 ? 2018');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('should handle edge case: leap year February 29', () => {
|
|
106
|
+
const date = new Date('2020-02-29T12:00:00Z');
|
|
107
|
+
const cron = generateCronExpression(date, 'UTC');
|
|
108
|
+
|
|
109
|
+
expect(cron).toBe('0 0 12 29 2 ? 2020');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a Quartz cron expression from a date.
|
|
3
|
+
* Format: second minute hour day month dayOfWeek year
|
|
4
|
+
* Example: "0 0 23 14 8 ? 2018"
|
|
5
|
+
*
|
|
6
|
+
* @param date - The date to convert to cron expression (Date object or ISO string)
|
|
7
|
+
* @param timezone - Optional timezone string (e.g., "Europe/Paris", "America/New_York")
|
|
8
|
+
* If provided, the date will be converted to the specified timezone.
|
|
9
|
+
* If not provided, the date will be used as-is.
|
|
10
|
+
* @returns A cron expression string in Quartz format
|
|
11
|
+
*/
|
|
12
|
+
export const generateCronExpression = (date: Date | string, timezone?: string): string => {
|
|
13
|
+
let dateObj: Date;
|
|
14
|
+
|
|
15
|
+
if (typeof date === 'string') {
|
|
16
|
+
dateObj = new Date(date);
|
|
17
|
+
} else {
|
|
18
|
+
dateObj = new Date(date);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (Number.isNaN(dateObj.getTime())) {
|
|
22
|
+
throw new TypeError('Invalid date provided');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// If timezone is provided, convert to that timezone
|
|
26
|
+
if (timezone) {
|
|
27
|
+
// Use Intl.DateTimeFormat to get the date components in the specified timezone
|
|
28
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
29
|
+
timeZone: timezone,
|
|
30
|
+
year: 'numeric',
|
|
31
|
+
month: '2-digit',
|
|
32
|
+
day: '2-digit',
|
|
33
|
+
hour: '2-digit',
|
|
34
|
+
minute: '2-digit',
|
|
35
|
+
second: '2-digit',
|
|
36
|
+
hour12: false,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const parts = formatter.formatToParts(dateObj);
|
|
40
|
+
const year = parseInt(parts.find((p) => p.type === 'year')?.value || '0', 10);
|
|
41
|
+
const month = parseInt(parts.find((p) => p.type === 'month')?.value || '0', 10);
|
|
42
|
+
const day = parseInt(parts.find((p) => p.type === 'day')?.value || '0', 10);
|
|
43
|
+
const hour = parseInt(parts.find((p) => p.type === 'hour')?.value || '0', 10);
|
|
44
|
+
const minute = parseInt(parts.find((p) => p.type === 'minute')?.value || '0', 10);
|
|
45
|
+
const second = parseInt(parts.find((p) => p.type === 'second')?.value || '0', 10);
|
|
46
|
+
|
|
47
|
+
// Format: second minute hour day month ? year
|
|
48
|
+
return `${second} ${minute} ${hour} ${day} ${month} ? ${year}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If no timezone, use local time components
|
|
52
|
+
const year = dateObj.getFullYear();
|
|
53
|
+
const month = dateObj.getMonth() + 1; // getMonth() returns 0-11, cron expects 1-12
|
|
54
|
+
const day = dateObj.getDate();
|
|
55
|
+
const hour = dateObj.getHours();
|
|
56
|
+
const minute = dateObj.getMinutes();
|
|
57
|
+
const second = dateObj.getSeconds();
|
|
58
|
+
|
|
59
|
+
// Format: second minute hour day month ? year
|
|
60
|
+
return `${second} ${minute} ${hour} ${day} ${month} ? ${year}`;
|
|
61
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, test, vi, expect, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { Client } from '@vulog/aima-client';
|
|
4
|
+
import { getUnavailabilities } from './getUnavailabilities';
|
|
5
|
+
import { Unavailability } from './types';
|
|
6
|
+
|
|
7
|
+
describe('getUnavailabilities', () => {
|
|
8
|
+
const getMock = vi.fn();
|
|
9
|
+
const client = {
|
|
10
|
+
get: getMock,
|
|
11
|
+
clientOptions: {
|
|
12
|
+
fleetId: 'FLEET_ID',
|
|
13
|
+
},
|
|
14
|
+
} as unknown as Client;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should return invalid args for invalid date format', async () => {
|
|
21
|
+
const options = {
|
|
22
|
+
from: 'invalid-date',
|
|
23
|
+
to: '2018-08-16T07:30:00Z',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
await expect(getUnavailabilities(client, options)).rejects.toThrow('Invalid args');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should return empty array on 404', async () => {
|
|
30
|
+
const options = {
|
|
31
|
+
from: '2018-08-14T07:30:00Z',
|
|
32
|
+
to: '2018-08-16T07:30:00Z',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
getMock.mockRejectedValueOnce({
|
|
36
|
+
formattedError: {
|
|
37
|
+
status: 404,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const result = await getUnavailabilities(client, options);
|
|
42
|
+
expect(result).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should get unavailabilities successfully with pagination', async () => {
|
|
46
|
+
const options = {
|
|
47
|
+
from: '2018-08-14T07:30:00Z',
|
|
48
|
+
to: '2018-08-16T07:30:00Z',
|
|
49
|
+
page: 0,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const mockResponsePage1: Unavailability[] = [
|
|
53
|
+
{
|
|
54
|
+
id: '550e8400-e29b-41d4-a716-446655440001',
|
|
55
|
+
cronExpression: '0 0 23 * * ? *',
|
|
56
|
+
duration: 60,
|
|
57
|
+
maintenanceTitle: 'GetAround booking 234535',
|
|
58
|
+
vehicleId: '550e8400-e29b-41d4-a716-446655440000',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const mockResponsePage2: Unavailability[] = [];
|
|
63
|
+
|
|
64
|
+
getMock
|
|
65
|
+
.mockResolvedValueOnce({
|
|
66
|
+
data: mockResponsePage1,
|
|
67
|
+
})
|
|
68
|
+
.mockResolvedValueOnce({
|
|
69
|
+
data: mockResponsePage2,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const result = await getUnavailabilities(client, options);
|
|
73
|
+
expect(result).toEqual(mockResponsePage1);
|
|
74
|
+
expect(getMock).toBeCalledTimes(2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should throw error when max pages reached', async () => {
|
|
78
|
+
const options = {
|
|
79
|
+
from: '2018-08-14T07:30:00Z',
|
|
80
|
+
to: '2018-08-16T07:30:00Z',
|
|
81
|
+
page: 0,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const mockResponse: Unavailability[] = [
|
|
85
|
+
{
|
|
86
|
+
id: '550e8400-e29b-41d4-a716-446655440001',
|
|
87
|
+
cronExpression: '0 0 23 * * ? *',
|
|
88
|
+
duration: 60,
|
|
89
|
+
maintenanceTitle: 'GetAround booking 234535',
|
|
90
|
+
vehicleId: '550e8400-e29b-41d4-a716-446655440000',
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// Mock 51 pages to trigger max pages error
|
|
95
|
+
for (let i = 0; i < 51; i++) {
|
|
96
|
+
getMock.mockResolvedValueOnce({
|
|
97
|
+
data: mockResponse,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await expect(getUnavailabilities(client, options)).rejects.toThrow('Maximum page limit');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('should throw error on non-404 errors', async () => {
|
|
105
|
+
const options = {
|
|
106
|
+
from: '2018-08-14T07:30:00Z',
|
|
107
|
+
to: '2018-08-16T07:30:00Z',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
getMock.mockRejectedValueOnce({
|
|
111
|
+
formattedError: {
|
|
112
|
+
status: 500,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await expect(getUnavailabilities(client, options)).rejects.toEqual({
|
|
117
|
+
formattedError: {
|
|
118
|
+
status: 500,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Client } from '@vulog/aima-client';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { Unavailability, UnavailabilityOptions } from './types';
|
|
5
|
+
|
|
6
|
+
const schema = z.object({
|
|
7
|
+
from: z.string().datetime({ offset: false, precision: 0 }),
|
|
8
|
+
to: z.string().datetime({ offset: false, precision: 0 }),
|
|
9
|
+
page: z.number().int().nonnegative().default(0),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const getUnavailabilities = async (
|
|
13
|
+
client: Client,
|
|
14
|
+
options: UnavailabilityOptions
|
|
15
|
+
): Promise<Unavailability[]> => {
|
|
16
|
+
const result = schema.safeParse({
|
|
17
|
+
from: options.from,
|
|
18
|
+
to: options.to,
|
|
19
|
+
page: options.page ?? 0,
|
|
20
|
+
});
|
|
21
|
+
if (!result.success) {
|
|
22
|
+
throw new TypeError('Invalid args', {
|
|
23
|
+
cause: result.error.issues,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const allUnavailabilities: Unavailability[] = [];
|
|
28
|
+
let currentPage = result.data.page;
|
|
29
|
+
let hasMorePages = true;
|
|
30
|
+
const MAX_PAGES = 50;
|
|
31
|
+
|
|
32
|
+
while (hasMorePages) {
|
|
33
|
+
if (currentPage >= MAX_PAGES) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Maximum page limit (${MAX_PAGES}) reached. This might indicate an issue with the pagination or a very large dataset.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const queryParams = new URLSearchParams({
|
|
40
|
+
from: result.data.from,
|
|
41
|
+
to: result.data.to,
|
|
42
|
+
page: currentPage.toString(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// eslint-disable-next-line no-await-in-loop -- Pagination requires sequential API calls
|
|
46
|
+
const unavailabilities = await client
|
|
47
|
+
.get<Unavailability[]>(
|
|
48
|
+
`/boapi/proxy/user/fleets/${client.clientOptions.fleetId}/scheduledRules/unavailability/vehicles?${queryParams.toString()}`
|
|
49
|
+
)
|
|
50
|
+
.then(({ data }) => data)
|
|
51
|
+
.catch((error) => {
|
|
52
|
+
if (error.formattedError?.status === 404) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
allUnavailabilities.push(...unavailabilities);
|
|
59
|
+
|
|
60
|
+
// Stop if we received fewer unavailabilities than expected (including 0)
|
|
61
|
+
// Assuming page size is consistent, stop when we get an empty array
|
|
62
|
+
hasMorePages = unavailabilities.length > 0;
|
|
63
|
+
currentPage += 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return allUnavailabilities;
|
|
67
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, test, vi, expect, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { Client } from '@vulog/aima-client';
|
|
4
|
+
import { getUnavailabilitiesByVehicle } from './getUnavailabilitiesByVehicle';
|
|
5
|
+
import { Unavailability } from './types';
|
|
6
|
+
|
|
7
|
+
describe('getUnavailabilitiesByVehicle', () => {
|
|
8
|
+
const getMock = vi.fn();
|
|
9
|
+
const client = {
|
|
10
|
+
get: getMock,
|
|
11
|
+
clientOptions: {
|
|
12
|
+
fleetId: 'FLEET_ID',
|
|
13
|
+
},
|
|
14
|
+
} as unknown as Client;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should return invalid args', async () => {
|
|
21
|
+
const vehicleId = 'INVALID_VEHICLE_ID';
|
|
22
|
+
|
|
23
|
+
await expect(getUnavailabilitiesByVehicle(client, vehicleId)).rejects.toThrow('Invalid args');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should return empty array on 404', async () => {
|
|
27
|
+
const vehicleId = '550e8400-e29b-41d4-a716-446655440000';
|
|
28
|
+
|
|
29
|
+
getMock.mockRejectedValueOnce({
|
|
30
|
+
formattedError: {
|
|
31
|
+
status: 404,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const result = await getUnavailabilitiesByVehicle(client, vehicleId);
|
|
36
|
+
expect(result).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('should get unavailabilities by vehicle successfully', async () => {
|
|
40
|
+
const vehicleId = '550e8400-e29b-41d4-a716-446655440000';
|
|
41
|
+
|
|
42
|
+
const mockResponse: Unavailability[] = [
|
|
43
|
+
{
|
|
44
|
+
id: '550e8400-e29b-41d4-a716-446655440001',
|
|
45
|
+
cronExpression: '0 0 23 * * ? *',
|
|
46
|
+
duration: 60,
|
|
47
|
+
maintenanceTitle: 'GetAround booking 234535',
|
|
48
|
+
vehicleId: '550e8400-e29b-41d4-a716-446655440000',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
getMock.mockResolvedValueOnce({
|
|
53
|
+
data: mockResponse,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await getUnavailabilitiesByVehicle(client, vehicleId);
|
|
57
|
+
expect(result).toEqual(mockResponse);
|
|
58
|
+
expect(getMock).toBeCalledWith(
|
|
59
|
+
`/boapi/proxy/user/fleets/FLEET_ID/scheduledRules/unavailability/vehicles/${vehicleId}`
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('should throw error on non-404 errors', async () => {
|
|
64
|
+
const vehicleId = '550e8400-e29b-41d4-a716-446655440000';
|
|
65
|
+
|
|
66
|
+
getMock.mockRejectedValueOnce({
|
|
67
|
+
formattedError: {
|
|
68
|
+
status: 500,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await expect(getUnavailabilitiesByVehicle(client, vehicleId)).rejects.toEqual({
|
|
73
|
+
formattedError: {
|
|
74
|
+
status: 500,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Client } from '@vulog/aima-client';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { Unavailability } from './types';
|
|
5
|
+
|
|
6
|
+
const schema = z.object({
|
|
7
|
+
vehicleId: z.string().trim().min(1).uuid(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const getUnavailabilitiesByVehicle = async (client: Client, vehicleId: string): Promise<Unavailability[]> => {
|
|
11
|
+
const result = schema.safeParse({ vehicleId });
|
|
12
|
+
if (!result.success) {
|
|
13
|
+
throw new TypeError('Invalid args', {
|
|
14
|
+
cause: result.error.issues,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return client
|
|
18
|
+
.get<Unavailability[]>(
|
|
19
|
+
`/boapi/proxy/user/fleets/${client.clientOptions.fleetId}/scheduledRules/unavailability/vehicles/${result.data.vehicleId}`
|
|
20
|
+
)
|
|
21
|
+
.then(({ data }) => data)
|
|
22
|
+
.catch((error) => {
|
|
23
|
+
if (error.formattedError?.status === 404) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
});
|
|
28
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { getUnavailabilitiesByVehicle } from './getUnavailabilitiesByVehicle';
|
|
2
|
+
export { getUnavailabilities } from './getUnavailabilities';
|
|
3
|
+
export { createUnavailability } from './createUnavailability';
|
|
4
|
+
export { updateUnavailability } from './updateUnavailability';
|
|
5
|
+
export { deleteUnavailability } from './deleteUnavailability';
|
|
6
|
+
export { generateCronExpression } from './generateCronExpression';
|
|
7
|
+
export * from './types';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type Unavailability = {
|
|
2
|
+
id: string;
|
|
3
|
+
cronExpression: string;
|
|
4
|
+
duration: number;
|
|
5
|
+
maintenanceTitle: string;
|
|
6
|
+
vehicleId: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type CreateUnavailabilityBody = {
|
|
10
|
+
cronExpression: string;
|
|
11
|
+
duration: number;
|
|
12
|
+
maintenanceTitle: string;
|
|
13
|
+
vehicleId: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type UpdateUnavailabilityBody = CreateUnavailabilityBody;
|
|
17
|
+
|
|
18
|
+
export type UnavailabilityOptions = {
|
|
19
|
+
from: string; // ISO date time
|
|
20
|
+
to: string; // ISO date time
|
|
21
|
+
page?: number;
|
|
22
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, test, vi, expect, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { Client } from '@vulog/aima-client';
|
|
4
|
+
import { updateUnavailability } from './updateUnavailability';
|
|
5
|
+
import { Unavailability } from './types';
|
|
6
|
+
|
|
7
|
+
describe('updateUnavailability', () => {
|
|
8
|
+
const putMock = vi.fn();
|
|
9
|
+
const client = {
|
|
10
|
+
put: putMock,
|
|
11
|
+
clientOptions: {
|
|
12
|
+
fleetId: 'FLEET_ID',
|
|
13
|
+
},
|
|
14
|
+
} as unknown as Client;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('should return invalid args for invalid id', async () => {
|
|
21
|
+
const id = 'INVALID_UUID';
|
|
22
|
+
const body = {
|
|
23
|
+
cronExpression: '0 0 23 * * ? *',
|
|
24
|
+
duration: 60,
|
|
25
|
+
maintenanceTitle: 'GetAround booking 234535',
|
|
26
|
+
vehicleId: '550e8400-e29b-41d4-a716-446655440000',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
await expect(updateUnavailability(client, id, body)).rejects.toThrow('Invalid args');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('should return invalid args for invalid body', async () => {
|
|
33
|
+
const id = '550e8400-e29b-41d4-a716-446655440001';
|
|
34
|
+
const body = {
|
|
35
|
+
cronExpression: '',
|
|
36
|
+
duration: -1,
|
|
37
|
+
maintenanceTitle: '',
|
|
38
|
+
vehicleId: 'INVALID_UUID',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
await expect(updateUnavailability(client, id, body)).rejects.toThrow('Invalid args');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should update unavailability successfully', async () => {
|
|
45
|
+
const id = '550e8400-e29b-41d4-a716-446655440001';
|
|
46
|
+
const body = {
|
|
47
|
+
cronExpression: '0 0 24 * * ? *',
|
|
48
|
+
duration: 120,
|
|
49
|
+
maintenanceTitle: 'GetAround booking 234535 updated',
|
|
50
|
+
vehicleId: '550e8400-e29b-41d4-a716-446655440000',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const mockResponse: Unavailability = {
|
|
54
|
+
id: id,
|
|
55
|
+
cronExpression: '0 0 24 * * ? *',
|
|
56
|
+
duration: 120,
|
|
57
|
+
maintenanceTitle: 'GetAround booking 234535 updated',
|
|
58
|
+
vehicleId: '550e8400-e29b-41d4-a716-446655440000',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
putMock.mockResolvedValueOnce({
|
|
62
|
+
data: mockResponse,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const result = await updateUnavailability(client, id, body);
|
|
66
|
+
expect(result).toEqual(mockResponse);
|
|
67
|
+
expect(putMock).toBeCalledWith(
|
|
68
|
+
`/boapi/proxy/user/fleets/FLEET_ID/scheduledRules/unavailability/${id}`,
|
|
69
|
+
{
|
|
70
|
+
cronExpression: body.cronExpression,
|
|
71
|
+
duration: body.duration,
|
|
72
|
+
maintenanceTitle: body.maintenanceTitle,
|
|
73
|
+
vehicleId: body.vehicleId,
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Client } from '@vulog/aima-client';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { UpdateUnavailabilityBody, Unavailability } from './types';
|
|
5
|
+
|
|
6
|
+
const schema = z.object({
|
|
7
|
+
id: z.string().trim().min(1).uuid(),
|
|
8
|
+
cronExpression: z.string().min(1),
|
|
9
|
+
duration: z.number().int().positive(),
|
|
10
|
+
maintenanceTitle: z.string().min(1),
|
|
11
|
+
vehicleId: z.string().uuid(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const updateUnavailability = async (
|
|
15
|
+
client: Client,
|
|
16
|
+
id: string,
|
|
17
|
+
body: UpdateUnavailabilityBody
|
|
18
|
+
): Promise<Unavailability> => {
|
|
19
|
+
const result = schema.safeParse({
|
|
20
|
+
id,
|
|
21
|
+
...body,
|
|
22
|
+
});
|
|
23
|
+
if (!result.success) {
|
|
24
|
+
throw new TypeError('Invalid args', {
|
|
25
|
+
cause: result.error.issues,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return client
|
|
29
|
+
.put<Unavailability>(
|
|
30
|
+
`/boapi/proxy/user/fleets/${client.clientOptions.fleetId}/scheduledRules/unavailability/${result.data.id}`,
|
|
31
|
+
{
|
|
32
|
+
cronExpression: result.data.cronExpression,
|
|
33
|
+
duration: result.data.duration,
|
|
34
|
+
maintenanceTitle: result.data.maintenanceTitle,
|
|
35
|
+
vehicleId: result.data.vehicleId,
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
.then(({ data }) => data);
|
|
39
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["src"],
|
|
3
|
+
"exclude": ["**/*.test.ts"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"target": "esnext",
|
|
7
|
+
"lib": ["esnext"],
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"moduleResolution": "node",
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"outDir": "dist",
|
|
14
|
+
"rootDir": "src"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
package/tsup.config.ts
ADDED