@controleonline/ui-common 1.2.87 → 1.2.89

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 (77) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +7 -156
  3. package/.github/workflows/publish-npm.yml +73 -27
  4. package/.github/workflows/pull-request-checks.yml +7 -5
  5. package/docs/technical/Device-Detail-Alias-UI-Sync.md +141 -0
  6. package/package.json +13 -8
  7. package/src/api/localDB.js +86 -131
  8. package/src/react/components/BottomNavigationBar.js +178 -174
  9. package/src/react/components/BottomNavigationBar.styles.js +139 -118
  10. package/src/react/components/DefaultProvider.native.js +427 -421
  11. package/src/react/components/DefaultProvider.web.js +131 -125
  12. package/src/react/components/RemoteCheckoutService.js +3 -3
  13. package/src/react/components/RuntimeFooterMarqueeText.js +88 -36
  14. package/src/react/components/RuntimeInfoFooter.js +76 -43
  15. package/src/react/components/RuntimeInfoFooter.styles.js +22 -14
  16. package/src/react/config/deviceConfigBootstrap.js +71 -233
  17. package/src/react/config/deviceConfigValues.js +238 -0
  18. package/src/react/pages/Devices/detail/DeviceChargePermissionSection.js +32 -0
  19. package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +4 -22
  20. package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +6 -2
  21. package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +4 -0
  22. package/src/react/pages/Devices/detail/DeviceDetailPdvConfigSection.js +7 -0
  23. package/src/react/pages/Devices/detail/DeviceDetailScreen.js +24 -335
  24. package/src/react/pages/Devices/detail/createDeviceDetailContext.js +20 -0
  25. package/src/react/pages/Devices/detail/deviceDetailHelpers.js +2 -11
  26. package/src/react/pages/Devices/detail/useDeviceDetailActions.js +11 -36
  27. package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +129 -37
  28. package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +11 -38
  29. package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +9 -28
  30. package/src/react/pages/IntegrationConfigFields.js +1 -0
  31. package/src/react/pages/IntegrationConfigPage.js +101 -45
  32. package/src/react/pages/IntegrationConfigPage.utils.js +21 -13
  33. package/src/react/pages/fetchPeopleConfigs.js +21 -0
  34. package/src/react/pages/integrationsCatalog.js +15 -110
  35. package/src/react/services/Cielo/Cielo.js +9 -9
  36. package/src/react/services/Getnet/Getnet.web.js +25 -0
  37. package/src/react/utils/companyAuthority.js +36 -0
  38. package/src/react/utils/confirmAction.js +12 -0
  39. package/src/react/utils/fileUrl.js +14 -5
  40. package/src/react/utils/paymentDevices.js +375 -270
  41. package/src/react/utils/printerDevices.js +2 -2
  42. package/src/react/utils/runtimeFooter.js +178 -224
  43. package/src/react/utils/runtimeFooterText.js +160 -0
  44. package/src/react/utils/runtimeLanguage.js +5 -5
  45. package/src/react/utils/runtimeZoom.js +29 -5
  46. package/src/react/utils/shopFranchises.js +167 -12
  47. package/src/react/utils/storeColumns.js +235 -217
  48. package/src/react/utils/systemErrorMessage.js +2 -1
  49. package/src/store/integration/index.js +43 -0
  50. package/src/tests/browser/manager/device-detail-apply-config-defined.spec.js +31 -0
  51. package/src/tests/browser/manager/device-detail-composition.spec.js +295 -0
  52. package/src/tests/browser/manager/device-detail-copy-config.spec.js +11 -1
  53. package/src/tests/browser/manager/device-detail-pdv-current-company.spec.js +273 -0
  54. package/src/tests/browser/manager/runtime-footer-rotation.spec.js +397 -0
  55. package/src/tests/react/components/DefaultProviderAuthority.test.js +28 -0
  56. package/src/tests/react/components/RuntimeFooterMarqueeText.build.test.js +29 -0
  57. package/src/tests/react/config/deviceConfigBootstrap.test.js +24 -1
  58. package/src/tests/react/pages/DeviceChargePermissionSection.test.js +42 -0
  59. package/src/tests/react/pages/applyCurrentDeviceConfig.test.js +85 -0
  60. package/src/tests/react/pages/deviceDetailComposition.test.js +65 -0
  61. package/src/tests/react/pages/deviceDetailHookDependencies.test.mjs +31 -0
  62. package/src/tests/react/pages/deviceDetailLoaderDependencies.test.mjs +28 -0
  63. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +27 -6
  64. package/src/tests/react/services/Cielo.test.js +2 -2
  65. package/src/tests/react/utils/companyAuthority.test.js +53 -0
  66. package/src/tests/react/utils/formatter.test.mjs +6 -0
  67. package/src/tests/react/utils/paymentDevices.test.js +72 -0
  68. package/src/tests/react/utils/runtimeFooter.test.js +35 -0
  69. package/src/tests/react/utils/runtimeFooterText.test.js +88 -0
  70. package/src/tests/react/utils/runtimeLanguage.test.js +4 -4
  71. package/src/tests/react/utils/runtimeZoom.test.js +20 -0
  72. package/src/tests/react/utils/shopFranchises.test.js +32 -13
  73. package/src/tests/react/utils/translate.test.mjs +6 -6
  74. package/src/utils/formatter.js +7 -3
  75. package/src/utils/translate.js +16 -16
  76. package/src/utils/translatePending.js +6 -6
  77. package/src/utils/translateStorage.js +2 -2
@@ -0,0 +1,65 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const {Linter} = require('eslint');
4
+ const parser = require('@babel/parser');
5
+ const traverse = require('@babel/traverse').default;
6
+ const createContext = require('../../../react/pages/Devices/detail/createDeviceDetailContext').default;
7
+
8
+ const directory = path.resolve(__dirname, '../../../react/pages/Devices/detail');
9
+ const files = fs.readdirSync(directory).filter(file => file.endsWith('.js'));
10
+
11
+ describe('DeviceDetail composition regression (app-community#706)', () => {
12
+ test.each(files)('%s parses and has no unbound runtime or JSX references', file => {
13
+ const source = fs.readFileSync(path.join(directory, file), 'utf8');
14
+ const messages = new Linter().verify(source, {
15
+ parserOptions: {ecmaVersion: 2022, sourceType: 'module', ecmaFeatures: {jsx: true}},
16
+ env: {browser: true, node: true, es2022: true},
17
+ rules: {'no-undef': 'error'},
18
+ });
19
+ expect(messages.map(message => `${message.line}: ${message.message}`)).toEqual([]);
20
+ const unbound = [];
21
+ traverse(parser.parse(source, {sourceType: 'module', plugins: ['jsx']}), {
22
+ JSXOpeningElement(element) {
23
+ const name = element.node.name;
24
+ if (name.type === 'JSXIdentifier' && /^[A-Z]/.test(name.name) &&
25
+ !element.scope.hasBinding(name.name)) unbound.push(name.name);
26
+ },
27
+ });
28
+ expect(unbound).toEqual([]);
29
+ });
30
+
31
+ test('section context retains actual values and callbacks instead of losing hook outputs', () => {
32
+ const setVisibility = jest.fn();
33
+ const setDelivery = jest.fn();
34
+ const saveDelivery = jest.fn();
35
+ const renderSwitchRow = jest.fn();
36
+ const state = {
37
+ currentCompany: {id: 7}, themeColors: {buttonText: '#fff'},
38
+ brandColors: {primary: '#000'}, runtimeDevice: {id: 403},
39
+ deviceOrderVisibility: 'company', setDeviceOrderVisibility: setVisibility,
40
+ deviceDeliveryEnabled: true, setDeviceDeliveryEnabled: setDelivery,
41
+ savingDeviceDeliverySettings: true, displayAutoPrintProductEnabled: true,
42
+ products: [{quantity: 2}], deviceRuntimeDebugInfoEnabled: true,
43
+ };
44
+ const context = createContext(state, {refreshCurrentConfig: jest.fn()}, {}, {
45
+ saveDeviceDeliverySettings: saveDelivery, renderSwitchRow, inflowTotal: 90,
46
+ });
47
+ expect(context.currentCompany).toBe(state.currentCompany);
48
+ expect(context.runtimeDevice).toBe(state.runtimeDevice);
49
+ expect(context.products).toBe(state.products);
50
+ expect(context.inflowTotal).toBe(90);
51
+ expect(context.palette).toBe(state.brandColors);
52
+ expect(context.orderVisibility).toBe('company');
53
+ expect(context.deliveryEnabled).toBe(true);
54
+ expect(context.savingDeliverySettings).toBe(true);
55
+ expect(context.displayAutoPrintProduct).toBe(true);
56
+ expect(context.runtimeDebugInfoEnabled).toBe(true);
57
+ context.setOrderVisibility('device');
58
+ context.setDeliveryEnabled(false);
59
+ context.saveDeviceDeliverySettings({deviceDeliveryEnabled: false});
60
+ expect(setVisibility).toHaveBeenCalledWith('device');
61
+ expect(setDelivery).toHaveBeenCalledWith(false);
62
+ expect(saveDelivery).toHaveBeenCalledWith({deviceDeliveryEnabled: false});
63
+ expect(context.renderSwitchRow).toBe(renderSwitchRow);
64
+ });
65
+ });
@@ -0,0 +1,31 @@
1
+ import assert from 'node:assert/strict';
2
+ import {readFileSync} from 'node:fs';
3
+ import test from 'node:test';
4
+
5
+ const hookFiles = [
6
+ 'useDeviceDetailActions.js',
7
+ 'useDeviceDetailSaves.js',
8
+ ];
9
+
10
+ for (const file of hookFiles) {
11
+ test(`${file} receives currentCompany from deps`, () => {
12
+ const source = readFileSync(
13
+ new URL(`../../../react/pages/Devices/detail/${file}`, import.meta.url),
14
+ 'utf8',
15
+ );
16
+ const dependencies = source.match(
17
+ /export default function [^(]+\(deps\)[\s\S]*?const \{([\s\S]*?)\}\s*=\s*deps;/,
18
+ );
19
+
20
+ assert.ok(dependencies, `${file} dependency block not found`);
21
+ const block = dependencies[1];
22
+ const hits = block.match(/(?:^|,)\s*currentCompany\s*(?:,|$)/gm) || [];
23
+
24
+ assert.equal(
25
+ hits.length,
26
+ 1,
27
+ `${file} must destructure currentCompany exactly once`,
28
+ );
29
+ assert.match(source, /currentCompany\?\.id/);
30
+ });
31
+ }
@@ -0,0 +1,28 @@
1
+ import assert from 'node:assert/strict';
2
+ import {readFileSync} from 'node:fs';
3
+ import test from 'node:test';
4
+
5
+ const loaderSource = readFileSync(
6
+ new URL(
7
+ '../../../react/pages/Devices/detail/useDeviceDetailLoaders.js',
8
+ import.meta.url,
9
+ ),
10
+ 'utf8',
11
+ );
12
+
13
+ test('device detail loaders receive currentCompany from deps exactly once', () => {
14
+ const dependencies = loaderSource.match(
15
+ /export default function useDeviceDetailLoaders\(deps\)[\s\S]*?const \{([\s\S]*?)\}\s*=\s*deps;/,
16
+ );
17
+
18
+ assert.ok(dependencies, 'useDeviceDetailLoaders dependency block not found');
19
+ const block = dependencies[1];
20
+ const hits = block.match(/(?:^|,)\s*currentCompany\s*(?:,|$)/gm) || [];
21
+ assert.equal(
22
+ hits.length,
23
+ 1,
24
+ `currentCompany must appear exactly once in deps destructure, found ${hits.length}`,
25
+ );
26
+ assert.match(loaderSource, /currentCompany\?\.id/);
27
+ assert.match(loaderSource, /filterDeviceConfigsByCompany\(items, currentCompany\?\.id\)/);
28
+ });
@@ -6,16 +6,20 @@ const catalog = fs.readFileSync(new URL('../../../react/pages/integrationsCatalo
6
6
  const page = fs.readFileSync(new URL('../../../react/pages/IntegrationConfigPage.js', import.meta.url), 'utf8');
7
7
  const fields = fs.readFileSync(new URL('../../../react/pages/IntegrationConfigFields.js', import.meta.url), 'utf8');
8
8
  const utils = fs.readFileSync(new URL('../../../react/pages/IntegrationConfigPage.utils.js', import.meta.url), 'utf8');
9
+ const fetchConfigs = fs.readFileSync(new URL('../../../react/pages/fetchPeopleConfigs.js', import.meta.url), 'utf8');
9
10
 
10
11
  test('Receita Federal catalog exposes company fiscal configuration', () => {
11
12
  assert.match(catalog, /key: 'receita-federal'/);
12
13
  assert.match(catalog, /label: 'Receita Federal'/);
13
14
  assert.match(catalog, /receita-federal-tax-regime/);
14
- assert.match(catalog, /receita-federal-ibge-code/);
15
15
  assert.match(catalog, /receita-federal-certificate-file/);
16
16
  assert.match(catalog, /receita-federal-certificate-password/);
17
17
  assert.match(catalog, /secureTextEntry: true/);
18
18
  assert.match(catalog, /\.pfx,\.p12,application\/x-pkcs12/);
19
+ assert.match(catalog, /key: 'general'/);
20
+ assert.match(catalog, /key: 'cte'/);
21
+ assert.doesNotMatch(catalog, /key: 'receita-federal-ibge-code'/);
22
+ assert.doesNotMatch(catalog, /key: 'receita-federal-(nfe|nfce|nfse|cte)-last-number'/);
19
23
  });
20
24
 
21
25
  test('Receita Federal route resolves without relying only on route params', () => {
@@ -25,12 +29,29 @@ test('Receita Federal route resolves without relying only on route params', () =
25
29
 
26
30
  test('embedded fiscal configuration keeps reads and writes on the company being edited', () => {
27
31
  assert.match(utils, /route\?\.params\?\.companyId/);
32
+ assert.match(utils, /route\?\.params\?\.clientId/);
28
33
  assert.match(utils, /resolveProviderId/);
29
- assert.match(utils, /resolveFallbackConfigs/);
30
- assert.match(page, /resolveProviderId\(\{ route, currentCompany \}\)/);
31
- assert.match(page, /api\.fetch\('\/configs', \{ params: \{ people: providerIri \} \}\)/);
32
- assert.match(page, /addManyConfigs\(\{ configs, people: providerIri/);
33
- assert.match(page, /syncConfigValues\(fallbackConfigs\)/);
34
+ assert.match(utils, /if \(embedded\) return ''/);
35
+ assert.doesNotMatch(utils, /resolveFallbackConfigs/);
36
+ assert.doesNotMatch(page, /fallbackConfigs/);
37
+ assert.match(page, /resolveProviderId\(\{ route, currentCompany, embedded \}\)/);
38
+ assert.match(page, /fetchPeopleConfigs/);
39
+ assert.match(page, /visibleFields/);
40
+ assert.match(page, /persistConfigs\(\{ configs, people: providerIri/);
41
+ });
42
+
43
+ test('fiscal tabs load all keys once; save only the current tab keys', () => {
44
+ assert.match(fetchConfigs, /configKey: keys/);
45
+ assert.match(fetchConfigs, /itemsPerPage: keys\.length/);
46
+ // Load all fiscal keys on mount/focus so tab switches do not re-fetch or toast.
47
+ assert.match(page, /allFieldKeys/);
48
+ assert.match(page, /needsMarketplaceIntegration/);
49
+ assert.match(page, /configKeys: keys/);
50
+ assert.match(page, /visibleFields\.map\(field => \(\{/);
51
+ assert.match(page, /Salvar \$\{activeTabDef\?\.label/);
52
+ assert.doesNotMatch(page, /itemsPerPage: 100/);
53
+ // Pure config (receita-federal) must not spam /marketplace/integrations on every tab.
54
+ assert.match(page, /needsMarketplaceIntegration = Boolean\(providerConfig\?\.oauthConnect\)/);
34
55
  });
35
56
 
36
57
  test('certificate picker remains scoped to the active company', () => {
@@ -50,7 +50,7 @@ const runtimeStores = merchantCode => ({
50
50
  }),
51
51
  },
52
52
  },
53
- defaultCompany: {id: 99},
53
+ mainCompany: {id: 99},
54
54
  },
55
55
  },
56
56
  configs: {
@@ -89,7 +89,7 @@ describe('CieloService', () => {
89
89
  people: {
90
90
  getters: {
91
91
  currentCompany: {id: 21, configs: {}},
92
- defaultCompany: {id: 99},
92
+ mainCompany: {id: 99},
93
93
  },
94
94
  },
95
95
  configs: {getters: {items: {}}},
@@ -0,0 +1,53 @@
1
+ import {describe, expect, it} from '@jest/globals';
2
+ import {
3
+ canAdministerCompany,
4
+ isSuperAdmin,
5
+ } from '../../../react/utils/companyAuthority';
6
+
7
+ describe('companyAuthority', () => {
8
+ it.each(['owner', 'manager'])(
9
+ 'treats %s of the main company as superadmin',
10
+ permission => {
11
+ const mainCompany = {permissions: [permission]};
12
+
13
+ expect(isSuperAdmin({mainCompany})).toBe(true);
14
+ expect(
15
+ canAdministerCompany({company: {permissions: []}, mainCompany}),
16
+ ).toBe(true);
17
+ },
18
+ );
19
+
20
+ it.each(['owner', 'manager'])(
21
+ 'treats %s of the current company as local admin',
22
+ permission => {
23
+ expect(
24
+ canAdministerCompany({
25
+ company: {permission: [permission]},
26
+ mainCompany: {permissions: []},
27
+ }),
28
+ ).toBe(true);
29
+ },
30
+ );
31
+
32
+ it.each(['director', 'employee'])(
33
+ 'does not treat %s as administrative authority',
34
+ permission => {
35
+ expect(
36
+ canAdministerCompany({
37
+ company: {permission: [permission]},
38
+ mainCompany: {permissions: []},
39
+ }),
40
+ ).toBe(false);
41
+ },
42
+ );
43
+
44
+ it('honors the global ROLE_SUPER resolved by the API', () => {
45
+ expect(
46
+ canAdministerCompany({
47
+ company: {permissions: []},
48
+ mainCompany: {permissions: []},
49
+ user: {roles: ['ROLE_SUPER']},
50
+ }),
51
+ ).toBe(true);
52
+ });
53
+ });
@@ -3,6 +3,12 @@ import test from 'node:test';
3
3
 
4
4
  import Formatter from '../../../utils/formatter.js';
5
5
 
6
+ test('normalizes optional text values', () => {
7
+ assert.equal(Formatter.normalizeText(' iFood '), 'iFood');
8
+ assert.equal(Formatter.normalizeText(null), '');
9
+ assert.equal(Formatter.normalizeText(undefined), '');
10
+ });
11
+
6
12
  test('formats API datetimes using the numeric offset returned by the backend', () => {
7
13
  assert.equal(
8
14
  Formatter.formatDateYmdTodmY('2026-07-22T00:00:00-02:00'),
@@ -86,3 +86,75 @@ describe('paymentDevices local Cielo print support', () => {
86
86
  ).toBe(false)
87
87
  })
88
88
  })
89
+
90
+ const {
91
+ buildWalletIdsForGateway,
92
+ selectPosWalletPaymentTypes,
93
+ walletPaymentTypeMatchesGateway,
94
+ } = require('../../../react/utils/paymentDevices')
95
+
96
+ describe('selectPosWalletPaymentTypes by gateway', () => {
97
+ const cieloDebit = {
98
+ id: 11,
99
+ wallet: {id: 10, wallet: 'Cielo'},
100
+ paymentType: {id: 5, paymentType: 'Debito'},
101
+ }
102
+ const infiniteDebit = {
103
+ id: 12,
104
+ wallet: {id: 20, wallet: 'Infinite Pay'},
105
+ paymentType: {id: 5, paymentType: 'Debito'},
106
+ }
107
+ const cash = {
108
+ id: 13,
109
+ wallet: {id: 30, wallet: 'Dinheiro'},
110
+ paymentType: {id: 1, paymentType: 'Dinheiro'},
111
+ }
112
+
113
+ it('builds wallet ids from discovered company configs', () => {
114
+ expect(
115
+ buildWalletIdsForGateway({
116
+ gateway: 'cielo',
117
+ companyConfigs: {
118
+ 'pos-cielo-wallet': '/wallets/10',
119
+ 'pos-cash-wallet': '/wallets/30',
120
+ },
121
+ }),
122
+ ).toEqual(['/wallets/10', '/wallets/30'])
123
+ })
124
+
125
+ it('keeps only Cielo and cash wallets for a Cielo POS', () => {
126
+ expect(
127
+ selectPosWalletPaymentTypes({
128
+ walletPaymentTypes: [cieloDebit, infiniteDebit, cash],
129
+ gateway: 'cielo',
130
+ companyConfigs: {
131
+ 'pos-cielo-wallet': 10,
132
+ 'pos-cash-wallet': 30,
133
+ },
134
+ }),
135
+ ).toEqual([cieloDebit, cash])
136
+ })
137
+
138
+ it('falls back to wallet name when discovery configs are still empty', () => {
139
+ expect(
140
+ selectPosWalletPaymentTypes({
141
+ walletPaymentTypes: [cieloDebit, infiniteDebit, cash],
142
+ gateway: 'cielo',
143
+ companyConfigs: {},
144
+ }),
145
+ ).toEqual([cieloDebit, cash])
146
+ })
147
+
148
+ it('does not mix Cielo types into another gateway', () => {
149
+ expect(
150
+ walletPaymentTypeMatchesGateway(cieloDebit, 'getnet'),
151
+ ).toBe(false)
152
+ expect(
153
+ selectPosWalletPaymentTypes({
154
+ walletPaymentTypes: [cieloDebit, infiniteDebit, cash],
155
+ gateway: 'infinite-pay',
156
+ companyConfigs: {},
157
+ }),
158
+ ).toEqual([infiniteDebit, cash])
159
+ })
160
+ })
@@ -7,6 +7,7 @@ const {
7
7
  getRuntimeFooterPrimaryText,
8
8
  getRuntimeFooterRotationEntries,
9
9
  getRuntimeFooterStoredVersion,
10
+ getRuntimeFooterText,
10
11
  getRuntimeFooterTextLines,
11
12
  getRuntimeFooterWebIdentifierCandidates,
12
13
  getRuntimeFooterWebHost,
@@ -447,6 +448,40 @@ test('rotates each footer line before the runtime version entry', () => {
447
448
  )
448
449
  })
449
450
 
451
+ test('isolates one footer line before the runtime version entry', () => {
452
+ const entries = getRuntimeFooterRotationEntries({
453
+ companyFooterText: 'Linha isolada do rodapé',
454
+ primaryText: 'Browser Manager 1.10.5',
455
+ })
456
+
457
+ assert.deepEqual(entries, ['Linha isolada do rodapé', 'Browser Manager 1.10.5'])
458
+ assert.equal(entries.some(entry => entry.includes(' • ')), false)
459
+ })
460
+
461
+ test('resolves configured footer text from company and device config shapes', () => {
462
+ assert.equal(
463
+ getRuntimeFooterText({
464
+ configs: {'device-runtime-footer-text': 'current company'},
465
+ }),
466
+ 'current company',
467
+ )
468
+ assert.equal(
469
+ getRuntimeFooterText(
470
+ {configs: {}},
471
+ [{configKey: 'device-runtime-footer-text', value: 'device config'}],
472
+ ),
473
+ 'device config',
474
+ )
475
+ assert.equal(
476
+ getRuntimeFooterText(
477
+ {configs: {}},
478
+ {'device-runtime-footer-text': ' line one \n line two '},
479
+ ),
480
+ 'line one\nline two',
481
+ )
482
+ assert.equal(getRuntimeFooterText({configs: {}}), '')
483
+ })
484
+
450
485
  test('hides runtime device information on the customer paylist only', () => {
451
486
  assert.equal(shouldShowRuntimeFooter('PaylistPage'), false)
452
487
  assert.equal(shouldShowRuntimeFooter('LoginPage'), true)
@@ -0,0 +1,88 @@
1
+ const assert = require('node:assert/strict')
2
+ const {test} = require('node:test')
3
+
4
+ const {
5
+ getRuntimeFooterRotationEntries,
6
+ getRuntimeFooterText,
7
+ getRuntimeFooterTextLines,
8
+ } = require('../../../react/utils/runtimeFooterText')
9
+
10
+ test('rotates a single footer line then primary without concatenating', () => {
11
+ const line = 'www.controleonline.com (11) 5555-5159'
12
+ const primaryText = 'web (203.0.113.42) / v1.3.6'
13
+ const entries = getRuntimeFooterRotationEntries({
14
+ companyFooterText: line,
15
+ primaryText,
16
+ })
17
+
18
+ assert.deepEqual(entries, [line, primaryText])
19
+ assert.equal(entries.some(entry => entry.includes(' • ')), false)
20
+ })
21
+
22
+ test('does not invent rotation entries when footer text is empty', () => {
23
+ assert.deepEqual(
24
+ getRuntimeFooterRotationEntries({
25
+ companyFooterText: '',
26
+ primaryText: 'PDV (229252771069b294) / v1.3.7',
27
+ }),
28
+ ['PDV (229252771069b294) / v1.3.7'],
29
+ )
30
+ })
31
+
32
+ test('keeps multi-line rotation plus primary', () => {
33
+ assert.deepEqual(
34
+ getRuntimeFooterRotationEntries({
35
+ companyFooterText: 'www.site.com\n(11) 99999-9999',
36
+ primaryText: 'web (203.0.113.42) / v1.3.6',
37
+ }),
38
+ ['www.site.com', '(11) 99999-9999', 'web (203.0.113.42) / v1.3.6'],
39
+ )
40
+ })
41
+
42
+ test('reads footer text from currentCompany map configs', () => {
43
+ assert.equal(
44
+ getRuntimeFooterText({
45
+ configs: {
46
+ 'device-runtime-footer-text': 'Linha currentCompany',
47
+ },
48
+ }),
49
+ 'Linha currentCompany',
50
+ )
51
+ })
52
+
53
+ test('falls back to extra configs when company has no footer text', () => {
54
+ assert.equal(
55
+ getRuntimeFooterText(
56
+ {configs: {}},
57
+ {
58
+ 'device-runtime-footer-text': 'Linha mainCompany',
59
+ },
60
+ ),
61
+ 'Linha mainCompany',
62
+ )
63
+ })
64
+
65
+ test('reads footer text from device_config array configs', () => {
66
+ assert.equal(
67
+ getRuntimeFooterText(null, [
68
+ {configKey: 'other', value: 'ignore'},
69
+ {configKey: 'device-runtime-footer-text', value: 'Linha device_config array'},
70
+ ]),
71
+ 'Linha device_config array',
72
+ )
73
+ })
74
+
75
+ test('reads footer text from device_config map configs', () => {
76
+ assert.equal(
77
+ getRuntimeFooterText(undefined, {
78
+ 'device-runtime-footer-text': {configValue: 'Linha device_config map'},
79
+ }),
80
+ 'Linha device_config map',
81
+ )
82
+ })
83
+
84
+ test('returns empty string when footer text is absent', () => {
85
+ assert.equal(getRuntimeFooterText({configs: {foo: 'bar'}}), '')
86
+ assert.equal(getRuntimeFooterText(null, []), '')
87
+ assert.deepEqual(getRuntimeFooterTextLines(''), [])
88
+ })
@@ -32,7 +32,7 @@ test('prefers the selected company language over cached config and session value
32
32
  code: 'en_US',
33
33
  },
34
34
  },
35
- defaultCompany: {
35
+ mainCompany: {
36
36
  language: {
37
37
  code: 'pt_BR',
38
38
  },
@@ -51,7 +51,7 @@ test('prefers the selected company language over cached config and session value
51
51
  test('falls back through default company, config, session and the hardcoded default', () => {
52
52
  assert.equal(
53
53
  resolveConfiguredLanguage({
54
- defaultCompany: {
54
+ mainCompany: {
55
55
  configs: {
56
56
  language: 'de_DE',
57
57
  },
@@ -86,7 +86,7 @@ test('builds the translation bootstrap key only from language and company contex
86
86
  buildTranslationBootstrapKey({
87
87
  language: ' PT_BR ',
88
88
  currentCompanyId: 21,
89
- defaultCompanyId: 1,
89
+ mainCompanyId: 1,
90
90
  }),
91
91
  'pt-br::21::1',
92
92
  )
@@ -94,7 +94,7 @@ test('builds the translation bootstrap key only from language and company contex
94
94
  buildTranslationBootstrapKey({
95
95
  language: 'pt-br',
96
96
  currentCompanyId: '',
97
- defaultCompanyId: 1,
97
+ mainCompanyId: 1,
98
98
  }),
99
99
  '',
100
100
  )
@@ -30,9 +30,29 @@ test('builds compensated container dimensions for native scaled content', () =>
30
30
  assert.deepEqual(
31
31
  buildRuntimeZoomStyle(70),
32
32
  {
33
+ alignSelf: 'flex-start',
33
34
  height: '142.85714285714286%',
34
35
  transform: [{scale: 0.7}],
36
+ transformOrigin: 'top left',
35
37
  width: '142.85714285714286%',
36
38
  },
37
39
  )
38
40
  })
41
+
42
+ test('uses viewport pixels on native so scale origin stays top-left without distortion', () => {
43
+ assert.deepEqual(
44
+ buildRuntimeZoomStyle(70, {viewport: {width: 360, height: 800}}),
45
+ {
46
+ alignSelf: 'flex-start',
47
+ height: 800 / 0.7,
48
+ transform: [{scale: 0.7}],
49
+ transformOrigin: 'top left',
50
+ width: 360 / 0.7,
51
+ },
52
+ )
53
+ })
54
+
55
+ test('does not apply style when zoom is 100 percent', () => {
56
+ assert.equal(buildRuntimeZoomStyle(100), null)
57
+ assert.equal(buildRuntimeZoomStyle(100, {isWeb: true}), null)
58
+ })
@@ -52,12 +52,12 @@ describe('shopFranchises', () => {
52
52
  {
53
53
  id: 21,
54
54
  alias: 'Centro',
55
- shopAddresses: [{id: 501, nickname: 'Loja Centro'}],
55
+ shopAddresses: [{id: 501, nickname: 'Loja Centro', latitude: null, longitude: null}],
56
56
  },
57
57
  ]);
58
58
  });
59
59
 
60
- it('loads franchises from people_links on both company and people sides', async () => {
60
+ it('uses an authorized company-side link without duplicate fallback requests', async () => {
61
61
  api.fetch
62
62
  .mockResolvedValueOnce({
63
63
  member: [
@@ -79,18 +79,11 @@ describe('shopFranchises', () => {
79
79
 
80
80
  const directory = await fetchShopFranchiseDirectory({companyId: 10});
81
81
 
82
+ expect(api.fetch).toHaveBeenCalledTimes(1);
82
83
  expect(api.fetch).toHaveBeenCalledWith('people_links', {
83
84
  params: {
84
- company: '/people/10',
85
- linkType: 'franchisee',
86
- itemsPerPage: 50,
87
- page: 1,
88
- },
89
- });
90
- expect(api.fetch).toHaveBeenCalledWith('people_links', {
91
- params: {
92
- people: '/people/10',
93
- linkType: 'franchisee',
85
+ company: '10',
86
+ linkType: ['franchisee'],
94
87
  itemsPerPage: 50,
95
88
  page: 1,
96
89
  },
@@ -98,10 +91,36 @@ describe('shopFranchises', () => {
98
91
  expect(directory[0].id).toBe(22);
99
92
  expect(directory[0].alias).toBe('Norte');
100
93
  expect(directory[0].shopAddresses).toEqual([
101
- {id: 601, nickname: 'Loja Norte'},
94
+ {id: 601, nickname: 'Loja Norte', latitude: null, longitude: null},
102
95
  ]);
103
96
  });
104
97
 
98
+ it('rejects unrelated or non-franchise links before loading addresses', async () => {
99
+ api.fetch.mockResolvedValueOnce({
100
+ member: [
101
+ {
102
+ id: 1,
103
+ linkType: 'franchisee',
104
+ company: {id: 999},
105
+ people: {id: 22, alias: 'Other tenant'},
106
+ },
107
+ {
108
+ id: 2,
109
+ linkType: 'supplier',
110
+ company: {id: 10},
111
+ people: {id: 23, alias: 'Wrong relation'},
112
+ },
113
+ ],
114
+ });
115
+ api.fetch.mockResolvedValueOnce({member: []});
116
+
117
+ const directory = await fetchAllShopFranchiseDirectory({companyId: 10});
118
+
119
+ expect(directory).toEqual([]);
120
+ expect(api.fetch).toHaveBeenCalledTimes(2);
121
+ expect(api.fetch).not.toHaveBeenCalledWith('addresses', expect.anything());
122
+ });
123
+
105
124
  it('loads every page when building the full franchise directory', async () => {
106
125
  // Dual-side people_links: company side page1 (full), page2 empty break;
107
126
  // people side empty.
@@ -216,10 +216,10 @@ test('falls back past empty current-company values to the main company', () => {
216
216
  installLocalStorage();
217
217
  const translateStore = createPendingTranslateStore();
218
218
  const currentCompany = {id: 2};
219
- const defaultCompany = {id: 1};
219
+ const mainCompany = {id: 1};
220
220
  const translate = new Translate(
221
- [defaultCompany, currentCompany],
222
- defaultCompany,
221
+ [mainCompany, currentCompany],
222
+ mainCompany,
223
223
  currentCompany,
224
224
  ['orders'],
225
225
  translateStore,
@@ -235,10 +235,10 @@ test('getResolveCompaniesToCache returns current and main companies', () => {
235
235
  installLocalStorage();
236
236
  const translateStore = createPendingTranslateStore();
237
237
  const currentCompany = {id: 2};
238
- const defaultCompany = {id: 1};
238
+ const mainCompany = {id: 1};
239
239
  const translate = new Translate(
240
- [defaultCompany, currentCompany],
241
- defaultCompany,
240
+ [mainCompany, currentCompany],
241
+ mainCompany,
242
242
  currentCompany,
243
243
  ['orders'],
244
244
  translateStore,
@@ -39,9 +39,13 @@ const MOJIBAKE_REPLACEMENTS = [
39
39
  ];
40
40
 
41
41
  export default class Formatter {
42
-
43
- /* ================= UTIL ================= */
44
-
42
+
43
+ /* ================= UTIL ================= */
44
+
45
+ static normalizeText(value) {
46
+ return String(value || '').trim();
47
+ }
48
+
45
49
  static onlyNumbers(value) {
46
50
  return String(value || '').replace(/\D/g, '');
47
51
  }