@controleonline/ui-customers 1.0.81 → 1.0.83

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.
Files changed (64) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/publish-npm.yml +91 -0
  3. package/AGENTS.md +12 -6
  4. package/docs/technical/Client-Details-Criar-Usuario.md +113 -0
  5. package/package.json +5 -4
  6. package/src/react/components/tabs/AddressesTab.js +240 -335
  7. package/src/react/components/tabs/DocumentAttachments.js +270 -0
  8. package/src/react/components/tabs/DocumentsTab.js +355 -463
  9. package/src/react/components/tabs/DocumentsTab.styles.js +123 -0
  10. package/src/react/components/tabs/EmployeeCreateFormFields.js +203 -0
  11. package/src/react/components/tabs/EmployeesTab.js +17 -171
  12. package/src/react/components/tabs/EmployeesTab.styles.js +220 -172
  13. package/src/react/components/tabs/GeneralTab.js +84 -46
  14. package/src/react/components/tabs/GeneralTab.styles.js +81 -77
  15. package/src/react/components/tabs/MediaTab.js +50 -5
  16. package/src/react/components/tabs/PeopleCategoriesPanel.js +161 -78
  17. package/src/react/components/tabs/SalesmanTab.js +1 -1
  18. package/src/react/components/tabs/UserFormModal.js +26 -2
  19. package/src/react/components/tabs/UsersTab.js +30 -19
  20. package/src/react/components/tabs/UsersTab.styles.js +257 -257
  21. package/src/react/components/tabs/UsersTabApiKeyModal.js +115 -0
  22. package/src/react/components/tabs/UsersTabUserModal.js +222 -0
  23. package/src/react/components/tabs/addressesTabHelpers.js +190 -0
  24. package/src/react/components/tabs/documentsTabHelpers.js +142 -0
  25. package/src/react/components/tabs/employeeContacts.js +3 -23
  26. package/src/react/components/tabs/employeeUnlink.js +7 -4
  27. package/src/react/components/tabs/employeesTabHelpers.js +33 -9
  28. package/src/react/components/tabs/generalTabHelpers.js +30 -8
  29. package/src/react/components/tabs/useGeneralTabPeopleLink.js +68 -0
  30. package/src/react/components/tabs/usersTabHelpers.js +24 -23
  31. package/src/react/pages/details.page.styles.js +114 -100
  32. package/src/react/pages/employees.js +4 -0
  33. package/src/react/utils/humanCompanyLinkTypes.js +28 -0
  34. package/src/tests/browser/manager/client-details-add-user-modal.spec.js +281 -0
  35. package/src/tests/browser/manager/client-details-address-create-coords.spec.js +414 -0
  36. package/src/tests/browser/manager/client-details-address-edit.spec.js +331 -0
  37. package/src/tests/browser/manager/client-details-contact-edit.spec.js +198 -0
  38. package/src/tests/browser/manager/client-details-create-contact.spec.js +306 -0
  39. package/src/tests/browser/manager/client-details-documents.spec.js +346 -0
  40. package/src/tests/browser/manager/client-details-identity-case.spec.js +227 -0
  41. package/src/tests/browser/manager/client-details-people-type.manifest.md +18 -0
  42. package/src/tests/browser/manager/client-details-people-type.spec.js +345 -0
  43. package/src/tests/browser/manager/client-details-supplier-product-create.spec.js +334 -0
  44. package/src/tests/browser/manager/client-details-users-tab-load.spec.js +194 -0
  45. package/src/tests/browser/manager/employees-salesman-after-sales.spec.js +441 -0
  46. package/src/tests/react/components/SalesmanTab.company.test.js +40 -0
  47. package/src/tests/react/components/tabs/UsersTab.styles.test.js +39 -0
  48. package/src/tests/react/components/tabs/addressesTabHelpers.test.js +78 -0
  49. package/src/tests/react/components/tabs/documentsTabContract.test.mjs +37 -0
  50. package/src/tests/react/components/tabs/documentsTabHelpers.test.mjs +93 -0
  51. package/src/tests/react/components/tabs/employeeContacts.test.js +46 -4
  52. package/src/tests/react/components/tabs/employeeLinkTypes.contract.test.mjs +52 -0
  53. package/src/tests/react/components/tabs/employeeUnlink.test.js +11 -11
  54. package/src/tests/react/components/tabs/employeesTabHelpers.media.test.js +2 -23
  55. package/src/tests/react/components/tabs/employeesTabHelpers.test.js +59 -0
  56. package/src/tests/react/components/tabs/generalTabHelpers.test.js +35 -0
  57. package/src/tests/react/components/tabs/peopleMediaTabs.test.js +217 -217
  58. package/src/tests/react/components/tabs/peopleType.contract.test.mjs +47 -0
  59. package/src/tests/react/components/tabs/resolveSeededLinkType.test.mjs +20 -0
  60. package/src/tests/react/components/tabs/salesmanTabMedia.no-redeclare.test.mjs +33 -0
  61. package/src/tests/react/components/tabs/usersTabHelpers.test.js +20 -0
  62. package/src/tests/react/components/tabs/usersTabHelpers.test.mjs +63 -0
  63. package/src/tests/react/pages/employees.test.js +2 -0
  64. package/src/tests/react/utils/humanCompanyLinkTypes.test.js +50 -0
@@ -0,0 +1,93 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ applyDocumentMask,
5
+ extractId,
6
+ isImageFile,
7
+ isPdfFile,
8
+ normalizeDocumentFiles,
9
+ removeMask,
10
+ toDocumentIri,
11
+ toFileIri,
12
+ toDocumentItem,
13
+ unwrapUploadFile,
14
+ } from '../../../../react/components/tabs/documentsTabHelpers.js';
15
+
16
+ test('extractId resolves numeric, string and IRI values', () => {
17
+ assert.equal(extractId(12), '12');
18
+ assert.equal(extractId('/documents/45'), '45');
19
+ assert.equal(extractId({ '@id': '/files/9', id: 9 }), '9');
20
+ assert.equal(extractId(null), '');
21
+ });
22
+
23
+ test('applyDocumentMask formats CPF and CNPJ', () => {
24
+ assert.equal(applyDocumentMask('12345678901', 'CPF'), '123.456.789-01');
25
+ assert.equal(applyDocumentMask('12345678000199', 'CNPJ'), '12.345.678/0001-99');
26
+ });
27
+
28
+ test('removeMask strips non-digits', () => {
29
+ assert.equal(removeMask('12.345.678/0001-99'), '12345678000199');
30
+ });
31
+
32
+ test('file type helpers detect image and pdf urls', () => {
33
+ assert.equal(isImageFile('https://cdn.example/file.PNG'), true);
34
+ assert.equal(isPdfFile('https://cdn.example/doc.pdf'), true);
35
+ assert.equal(isPdfFile('https://cdn.example/doc.png'), false);
36
+ });
37
+
38
+ test('IRI helpers build resource paths', () => {
39
+ assert.equal(toDocumentIri(15), '/documents/15');
40
+ assert.equal(toFileIri({ id: 3 }), '/files/3');
41
+ assert.equal(toFileIri('/files/8'), '/files/8');
42
+ });
43
+
44
+ test('normalizeDocumentFiles prefers documentFiles collection over legacy file', () => {
45
+ const resolveUrl = file => file?.url || '';
46
+ const document = {
47
+ id: 1,
48
+ document: '123',
49
+ documentType: { '@id': '/document_types/1', documentType: 'CNPJ' },
50
+ file: { id: 99, url: 'https://cdn.example/legacy.pdf' },
51
+ documentFiles: [
52
+ {
53
+ id: 10,
54
+ file: { id: 20, name: 'cartao-cnpj.png', url: 'https://cdn.example/cartao.png' },
55
+ },
56
+ {
57
+ id: 11,
58
+ file: { id: 21, name: 'contrato.pdf', url: 'https://cdn.example/contrato.pdf' },
59
+ },
60
+ ],
61
+ };
62
+
63
+ const files = normalizeDocumentFiles(document, resolveUrl);
64
+ assert.equal(files.length, 2);
65
+ assert.equal(files[0].name, 'cartao-cnpj.png');
66
+ assert.equal(files[1].name, 'contrato.pdf');
67
+ assert.equal(files[0].legacy, false);
68
+
69
+ const legacyOnly = normalizeDocumentFiles(
70
+ { file: { id: 5, name: 'rg.jpg', url: 'https://cdn.example/rg.jpg' } },
71
+ resolveUrl,
72
+ );
73
+ assert.equal(legacyOnly.length, 1);
74
+ assert.equal(legacyOnly[0].legacy, true);
75
+ });
76
+
77
+ test('toDocumentItem maps API payload', () => {
78
+ const item = toDocumentItem({
79
+ '@id': '/documents/7',
80
+ id: 7,
81
+ document: '999',
82
+ documentType: { '@id': '/document_types/2' },
83
+ });
84
+ assert.equal(item.id, '7');
85
+ assert.equal(item.value, '999');
86
+ assert.equal(item.type, '/document_types/2');
87
+ });
88
+
89
+ test('unwrapUploadFile accepts hydra and nested shapes', () => {
90
+ assert.deepEqual(unwrapUploadFile({ file: { id: 1 } }), { id: 1 });
91
+ assert.deepEqual(unwrapUploadFile({ 'hydra:member': [{ id: 2 }] }), { id: 2 });
92
+ assert.deepEqual(unwrapUploadFile({ member: [{ id: 3 }] }), { id: 3 });
93
+ });
@@ -7,6 +7,7 @@ const {
7
7
  extractPeopleLinkItems,
8
8
  filterPeopleLinksByScope,
9
9
  normalizeEmployeeLinkType,
10
+ EMPLOYEE_CONTACT_LINK_TYPES,
10
11
  } = require('../../../../react/components/tabs/employeeContacts')
11
12
 
12
13
  describe('employeeContacts', () => {
@@ -21,9 +22,23 @@ describe('employeeContacts', () => {
21
22
  it('normalizes supported link types and falls back to employee', () => {
22
23
  expect(normalizeEmployeeLinkType('OWNER')).toBe('owner')
23
24
  expect(normalizeEmployeeLinkType('courier')).toBe('courier')
25
+ expect(normalizeEmployeeLinkType('salesman')).toBe('salesman')
26
+ expect(normalizeEmployeeLinkType('after-sales')).toBe('after-sales')
24
27
  expect(normalizeEmployeeLinkType('unknown')).toBe('employee')
25
28
  })
26
29
 
30
+ it('exports the same catalog used by create and edit pickers', () => {
31
+ expect(EMPLOYEE_CONTACT_LINK_TYPES).toEqual([
32
+ 'employee',
33
+ 'owner',
34
+ 'director',
35
+ 'manager',
36
+ 'salesman',
37
+ 'after-sales',
38
+ 'courier',
39
+ ])
40
+ })
41
+
27
42
  it('builds numeric relationship filters for collection reads', () => {
28
43
  expect(
29
44
  buildPeopleLinkReadParams({
@@ -38,16 +53,16 @@ describe('employeeContacts', () => {
38
53
  })
39
54
  })
40
55
 
41
- it('accepts linkTypes allowlist and itemsPerPage for contact reads (#636)', () => {
56
+ it('accepts linkTypes allowlist and itemsPerPage for contact reads (#636/#649)', () => {
42
57
  expect(
43
58
  buildPeopleLinkReadParams({
44
59
  companyId: 3,
45
- linkTypes: ['employee', 'owner', 'director', 'manager', 'courier'],
60
+ linkTypes: EMPLOYEE_CONTACT_LINK_TYPES,
46
61
  itemsPerPage: 100,
47
62
  }),
48
63
  ).toEqual({
49
64
  company: '3',
50
- linkType: ['employee', 'owner', 'director', 'manager', 'courier'],
65
+ linkType: EMPLOYEE_CONTACT_LINK_TYPES,
51
66
  itemsPerPage: 100,
52
67
  })
53
68
  })
@@ -84,13 +99,40 @@ describe('employeeContacts', () => {
84
99
  peopleLink: expect.objectContaining({id: 3229}),
85
100
  }),
86
101
  expect.objectContaining({
87
- id: '7',
102
+ id: 7,
88
103
  linkType: 'owner',
89
104
  name: 'Owner',
90
105
  }),
91
106
  ])
92
107
  })
93
108
 
109
+ it('keeps salesman and after-sales contacts in the company list (#649)', () => {
110
+ expect(
111
+ buildEmployeeContactsFromPeopleLinks(
112
+ {
113
+ member: [
114
+ {
115
+ id: 80,
116
+ linkType: 'salesman',
117
+ company: {id: 3},
118
+ people: {id: 81, name: 'Vendedor', peopleType: 'F'},
119
+ },
120
+ {
121
+ id: 82,
122
+ linkType: 'after-sales',
123
+ company: {id: 3},
124
+ people: {id: 83, name: 'Pos Venda', peopleType: 'F'},
125
+ },
126
+ ],
127
+ },
128
+ {parentPeopleId: '3'},
129
+ ),
130
+ ).toEqual([
131
+ expect.objectContaining({id: 81, linkType: 'salesman'}),
132
+ expect.objectContaining({id: 83, linkType: 'after-sales'}),
133
+ ])
134
+ })
135
+
94
136
  it('rejects links returned outside the requested relationship scope', () => {
95
137
  const requestedLink = {
96
138
  id: 11,
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Contract: Contatos create/edit share the same human company link-type catalog.
3
+ * Refs: app-community#446 #648 #649
4
+ */
5
+ import assert from 'node:assert/strict';
6
+ import test from 'node:test';
7
+ import { readFileSync } from 'node:fs';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { dirname, join } from 'node:path';
10
+
11
+ const root = join(dirname(fileURLToPath(import.meta.url)), '../../../../..');
12
+
13
+ const read = relativePath => readFileSync(join(root, relativePath), 'utf8');
14
+
15
+ const EXPECTED = [
16
+ 'employee',
17
+ 'owner',
18
+ 'director',
19
+ 'manager',
20
+ 'salesman',
21
+ 'after-sales',
22
+ 'courier',
23
+ ];
24
+
25
+ test('canonical catalog includes every human company link type', () => {
26
+ const src = read('src/react/utils/humanCompanyLinkTypes.js');
27
+ for (const type of EXPECTED) {
28
+ assert.match(src, new RegExp(`['"]${type}['"]`), `missing ${type}`);
29
+ }
30
+ });
31
+
32
+ test('create and edit helpers reuse the same catalog module', () => {
33
+ const createSrc = read('src/react/components/tabs/employeesTabHelpers.js');
34
+ const editSrc = read('src/react/components/tabs/generalTabHelpers.js');
35
+ const contactsSrc = read('src/react/components/tabs/employeeContacts.js');
36
+
37
+ assert.match(createSrc, /humanCompanyLinkTypes/);
38
+ assert.match(editSrc, /humanCompanyLinkTypes/);
39
+ assert.match(contactsSrc, /humanCompanyLinkTypes/);
40
+ assert.match(createSrc, /LINK_TYPE_OPTIONS/);
41
+ assert.match(editSrc, /LINK_TYPE_OPTIONS/);
42
+ });
43
+
44
+ test('employees page default context covers all human company roles', () => {
45
+ const src = read('src/react/pages/employees.js');
46
+ assert.match(src, /HUMAN_COMPANY_LINK_TYPES/);
47
+ assert.match(src, /DEFAULT_EMPLOYEE_CONTEXT_TYPES/);
48
+ assert.match(src, /salesman/);
49
+ assert.match(src, /after-sales/);
50
+ assert.match(src, /Cadastro de Vendedor/);
51
+ assert.match(src, /Cadastro de Pos-venda/);
52
+ });
@@ -16,9 +16,9 @@ describe('employeeUnlink', () => {
16
16
  ).toEqual({peopleId: '80', linkId: '201'});
17
17
  });
18
18
 
19
- it('prefers people.remove and falls back to people_link.remove', async () => {
20
- const removePeople = jest.fn().mockResolvedValue(null);
21
- const removePeopleLink = jest.fn();
19
+ it('prefers people_link.remove and falls back to people.remove', async () => {
20
+ const removePeople = jest.fn();
21
+ const removePeopleLink = jest.fn().mockResolvedValue(null);
22
22
  await expect(
23
23
  removeEmployeeFromCompany({
24
24
  peopleId: '80',
@@ -26,18 +26,18 @@ describe('employeeUnlink', () => {
26
26
  removePeople,
27
27
  removePeopleLink,
28
28
  }),
29
- ).resolves.toEqual({mode: 'people', peopleId: '80', linkId: '201'});
30
- expect(removePeople).toHaveBeenCalledWith('80');
31
- expect(removePeopleLink).not.toHaveBeenCalled();
29
+ ).resolves.toEqual({mode: 'link', peopleId: '80', linkId: '201'});
30
+ expect(removePeopleLink).toHaveBeenCalledWith('201');
31
+ expect(removePeople).not.toHaveBeenCalled();
32
32
 
33
33
  await expect(
34
34
  removeEmployeeFromCompany({
35
- peopleId: '',
36
- linkId: '201',
37
- removePeople: undefined,
38
- removePeopleLink: jest.fn().mockResolvedValue(null),
35
+ peopleId: '80',
36
+ linkId: '',
37
+ removePeople: jest.fn().mockResolvedValue(null),
38
+ removePeopleLink: undefined,
39
39
  }),
40
- ).resolves.toMatchObject({mode: 'link', linkId: '201'});
40
+ ).resolves.toMatchObject({mode: 'people', peopleId: '80'});
41
41
  });
42
42
 
43
43
  it('filters the local list after unlink', () => {
@@ -8,7 +8,7 @@ jest.mock('@controleonline/ui-common/src/react/utils/fileUrl', () => ({
8
8
  }), { virtual: true });
9
9
 
10
10
  jest.mock('@controleonline/ui-common/src/react/utils/entityDisplay', () => ({
11
- formatDisplayUppercase: value => String(value || '').toUpperCase(),
11
+ normalizeText: value => String(value || '').trim(),
12
12
  }), { virtual: true });
13
13
 
14
14
  describe('fetchPeopleMediaUrls batch', () => {
@@ -36,24 +36,8 @@ describe('fetchPeopleMediaUrls batch', () => {
36
36
  });
37
37
  });
38
38
 
39
- it('falls back to per-id calls when batch rejects', async () => {
40
- const getPeopleMedia = jest
41
- .fn()
42
- .mockRejectedValueOnce(new Error('batch unsupported'))
43
- .mockResolvedValueOnce([{ people: { id: 1 }, file: { url: 'https://cdn/1.png' } }])
44
- .mockResolvedValueOnce([{ people: { id: 2 }, file: { url: 'https://cdn/2.png' } }]);
45
-
46
- const result = await fetchPeopleMediaUrls({
47
- peopleActions: { getPeopleMedia },
48
- mediaType: 'avatar',
49
- peopleIds: [1, 2],
50
- });
51
-
52
39
  expect(getPeopleMedia).toHaveBeenCalledTimes(3);
53
- expect(result).toEqual({
54
- '1': 'https://cdn/1.png',
55
- '2': 'https://cdn/2.png',
56
- });
40
+ expect(result).toEqual({ '1': 'https://cdn/1.png' });
57
41
  });
58
42
  });
59
43
 
@@ -70,9 +54,4 @@ describe('extractPeopleMediaUrl from people_links payload', () => {
70
54
  });
71
55
  expect(url).toBe('https://cdn/avatar.png');
72
56
  });
73
-
74
- it('returns empty string when peopleMedia is missing', () => {
75
- expect(extractPeopleMediaUrl({ id: 10 })).toBe('');
76
- expect(extractPeopleMediaUrl(null)).toBe('');
77
- });
78
57
  });
@@ -8,6 +8,10 @@ import {
8
8
  buildEmployeeCreatePayload,
9
9
  extractId,
10
10
  LINK_TYPE_OPTIONS,
11
+ PEOPLE_TYPE_OPTIONS,
12
+ normalizePeopleType,
13
+ formatPeopleTypeLabel,
14
+ formatEmployeeContactMeta,
11
15
  buildEmployeeDetailNavParams,
12
16
  } from '../../../../react/components/tabs/employeesTabHelpers';
13
17
 
@@ -37,9 +41,64 @@ describe('employeesTabHelpers', () => {
37
41
  expect(payload.peopleType).toBe('F');
38
42
  });
39
43
 
44
+ it('buildEmployeeCreatePayload accepts salesman and after-sales (#649)', () => {
45
+ expect(
46
+ buildEmployeeCreatePayload({
47
+ name: 'VEN',
48
+ alias: 'V',
49
+ linkType: 'salesman',
50
+ parentPeopleId: '15',
51
+ }).linkType,
52
+ ).toBe('salesman');
53
+ expect(
54
+ buildEmployeeCreatePayload({
55
+ name: 'POS',
56
+ alias: 'P',
57
+ linkType: 'after-sales',
58
+ parentPeopleId: '15',
59
+ }).linkType,
60
+ ).toBe('after-sales');
61
+ });
62
+
63
+ it('buildEmployeeCreatePayload accepts peopleType J (PJ)', () => {
64
+ const payload = buildEmployeeCreatePayload({
65
+ name: 'EMPRESA X',
66
+ alias: 'X',
67
+ linkType: 'employee',
68
+ parentPeopleId: '15',
69
+ peopleType: 'J',
70
+ });
71
+ expect(payload.peopleType).toBe('J');
72
+ });
73
+
74
+ it('normalizePeopleType only allows F|J', () => {
75
+ expect(normalizePeopleType('F')).toBe('F');
76
+ expect(normalizePeopleType('j')).toBe('J');
77
+ expect(normalizePeopleType('pj')).toBe('J');
78
+ expect(normalizePeopleType('contrato')).toBe('F');
79
+ expect(normalizePeopleType('')).toBe('F');
80
+ expect(PEOPLE_TYPE_OPTIONS.map(o => o.value)).toEqual(['F', 'J']);
81
+ expect(formatPeopleTypeLabel('J')).toBe('Pessoa Jurídica');
82
+ });
83
+
84
+ it('formatEmployeeContactMeta includes peopleType', () => {
85
+ expect(
86
+ formatEmployeeContactMeta({ id: 9, peopleType: 'J', linkType: 'employee' }),
87
+ ).toContain('Pessoa Jurídica');
88
+ });
89
+
40
90
  it('extractId and LINK_TYPE_OPTIONS', () => {
41
91
  expect(extractId('/people/3')).toBe('3');
42
92
  expect(LINK_TYPE_OPTIONS.some(o => o.value === 'employee')).toBe(true);
93
+ expect(LINK_TYPE_OPTIONS.map(o => o.value)).toEqual([
94
+ 'employee',
95
+ 'owner',
96
+ 'director',
97
+ 'manager',
98
+ 'salesman',
99
+ 'after-sales',
100
+ 'courier',
101
+ ]);
43
102
  });
44
103
 
45
104
  it('buildEmployeeDetailNavParams opens employee detail on general tab', () => {
@@ -3,7 +3,11 @@
3
3
  */
4
4
  import {
5
5
  normalizeEnable,
6
+ normalizeIdentityValue,
6
7
  normalizeLinkType,
8
+ resolveSeededLinkType,
9
+ normalizePeopleType,
10
+ PEOPLE_TYPE_OPTIONS,
7
11
  formatYmdToBr,
8
12
  parseBrDateToYmd,
9
13
  formatDateInput,
@@ -13,6 +17,16 @@ import {
13
17
  } from '../../../components/tabs/generalTabHelpers';
14
18
 
15
19
  describe('generalTabHelpers', () => {
20
+ describe('normalizeIdentityValue', () => {
21
+ it('preserves mixed case typed by the user', () => {
22
+ expect(normalizeIdentityValue('Cláudia Silva')).toBe('Cláudia Silva');
23
+ expect(normalizeIdentityValue(' ACME Ltda ')).toBe('ACME Ltda');
24
+ expect(normalizeIdentityValue('kibelicia comida árabe')).toBe('kibelicia comida árabe');
25
+ });
26
+ it('collapses internal whitespace without changing case', () => {
27
+ expect(normalizeIdentityValue('Ana Maria')).toBe('Ana Maria');
28
+ });
29
+ });
16
30
  describe('normalizeEnable', () => {
17
31
  it('accepts boolean true/false', () => {
18
32
  expect(normalizeEnable(true)).toBe(true);
@@ -49,6 +63,8 @@ describe('generalTabHelpers', () => {
49
63
  it('is case-insensitive', () => {
50
64
  expect(normalizeLinkType('OWNER')).toBe('owner');
51
65
  expect(normalizeLinkType(' Manager ')).toBe('manager');
66
+ expect(normalizeLinkType('SALESMAN')).toBe('salesman');
67
+ expect(normalizeLinkType(' After-Sales ')).toBe('after-sales');
52
68
  });
53
69
  });
54
70
 
@@ -127,4 +143,23 @@ describe('generalTabHelpers', () => {
127
143
  ).toBe(false);
128
144
  });
129
145
  });
146
+
147
+
148
+ describe('resolveSeededLinkType', () => {
149
+ it('prefers client.linkType over route seed after save', () => {
150
+ expect(resolveSeededLinkType('courier', 'owner')).toBe('courier');
151
+ expect(resolveSeededLinkType('', 'owner')).toBe('owner');
152
+ expect(resolveSeededLinkType(undefined, 'manager')).toBe('manager');
153
+ });
154
+ });
155
+
156
+ describe('normalizePeopleType', () => {
157
+ it('maps only F and J from API enum', () => {
158
+ expect(normalizePeopleType('F')).toBe('F');
159
+ expect(normalizePeopleType('J')).toBe('J');
160
+ expect(normalizePeopleType('juridica')).toBe('J');
161
+ expect(normalizePeopleType('contrato')).toBe('F');
162
+ expect(PEOPLE_TYPE_OPTIONS.map(o => o.value)).toEqual(['F', 'J']);
163
+ });
164
+ });
130
165
  });