Skip to content

Commit d6c88ca

Browse files
authored
feat: promote to main (#350)
2 parents 483d785 + 4d845aa commit d6c88ca

26 files changed

+2176
-397
lines changed

openApi/openapi.merchants.portal.yml

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,14 @@ paths:
496496
type: string
497497
maxLength: 14
498498
pattern: ^[a-zA-Z0-9]{1,14}$
499+
- name: trxCode
500+
in: query
501+
description: "ENG: The transaction's code - IT: Codice della transazione"
502+
required: false
503+
schema:
504+
type: string
505+
maxLength: 100
506+
pattern: '^[A-Z0-9]+$'
499507
responses:
500508
"200":
501509
description: Ok
@@ -1145,6 +1153,14 @@ paths:
11451153
required: false
11461154
schema:
11471155
type: string
1156+
- name: trxCode
1157+
in: query
1158+
description: "ENG: The transaction's code - IT: Codice della transazione"
1159+
required: false
1160+
schema:
1161+
type: string
1162+
maxLength: 100
1163+
pattern: '^[A-Z0-9]+$'
11481164
responses:
11491165
'200':
11501166
description: Ok
@@ -2008,8 +2024,10 @@ components:
20082024
rewardBatchTrxStatus:
20092025
$ref: "#/components/schemas/RewardBatchTrxStatus"
20102026
rewardBatchRejectionReason:
2011-
type: string
2012-
description: Reason why the transaction was rejected from the reward batch, if any
2027+
type: array
2028+
items:
2029+
$ref: "#/components/schemas/ReasonDTO"
2030+
description: "ENG: Reason associated with TRX status - IT: Motivo associato allo stato della trx"
20132031
rewardBatchInclusionDate:
20142032
type: string
20152033
format: date-time
@@ -2689,16 +2707,16 @@ components:
26892707
format: date-time
26902708
description: 'ENG: The date of the transaction - IT: La data della transazione'
26912709
maxLength: 255
2692-
trxCode:
2693-
type: string
2694-
description: "ENG: Transaction code - IT: Il codice della transazione"
2695-
maxLength: 100
2696-
pattern: "^[a-zA-Z0-9]+$"
26972710
trxChargeDate:
26982711
type: string
26992712
format: date-time
27002713
description: "ENG: Payment authorization date - IT: La data di autorizzazione del pagamento"
27012714
maxLength: 255
2715+
trxCode:
2716+
type: string
2717+
description: "ENG: Transaction code - IT: Il codice della transazione"
2718+
maxLength: 100
2719+
pattern: "^[a-zA-Z0-9]+$"
27022720
updateDate:
27032721
type: string
27042722
format: date-time
@@ -2966,10 +2984,20 @@ components:
29662984
required:
29672985
- franchiseName
29682986
- pointOfSaleId
2987+
ReasonDTO:
2988+
type: object
2989+
properties:
2990+
date:
2991+
type: string
2992+
format: date-time
2993+
example: "2026-01-30T10:00:00Z"
2994+
reason:
2995+
type: string
2996+
example: "Invoice error"
29692997
securitySchemes:
29702998
Bearer:
29712999
type: apiKey
29723000
name: Authorization
29733001
in: header
29743002
security:
2975-
- Bearer: []
3003+
- Bearer: []

src/__tests__/helpers.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import {
88
isValidEmail,
99
isValidUrl,
1010
generateUniqueId,
11+
handlePromptMessage,
12+
truncateString,
13+
formatEuro
1114
} from '../helpers';
1215
import { MISSING_DATA_PLACEHOLDER, MISSING_EURO_PLACEHOLDER } from '../utils/constants';
1316

@@ -202,3 +205,40 @@ describe('generateUniqueId', () => {
202205
expect(generateUniqueId()).toBe('17000000000004f25k6o');
203206
});
204207
});
208+
209+
describe('handlePromptMessage', () => {
210+
test('should handle prompt message', () => {
211+
sessionStorage.setItem('storesPagination', 'test')
212+
const pathName1 = { pathname: "path-name-test" }
213+
const targetPage1 = "target-page-test"
214+
215+
const pathName2 = { pathname: "path-name-test" }
216+
const targetPage2 = "path-name-test"
217+
218+
handlePromptMessage(pathName2, targetPage2)
219+
expect(sessionStorage.getItem('storesPagination')).toBe('test')
220+
221+
handlePromptMessage(pathName1, targetPage1)
222+
expect(sessionStorage.getItem('storesPagination')).toBeNull
223+
});
224+
});
225+
226+
describe('formatEuro', () => {
227+
test('should formatEuro', () => {
228+
const amountCents = 4500
229+
const amount = formatEuro(amountCents)
230+
expect(amount).toBe("45,00€")
231+
});
232+
});
233+
234+
describe('truncateString', () => {
235+
test('should truncateString', () => {
236+
const initialText = "Initial text excess"
237+
const finalText = truncateString(initialText, 7)
238+
const fullText = truncateString(initialText)
239+
const emptyText = truncateString()
240+
expect(finalText).toBe("Initial...")
241+
expect(fullText).toBe("Initial text excess")
242+
expect(emptyText).toBe("-")
243+
});
244+
});

src/api/MerchantsApiClient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export const MerchantApi = {
8787
rewardBatchId?: string;
8888
rewardBatchTrxStatus?: string;
8989
pointOfSaleId?: string;
90+
trxCode?: string;
9091
}
9192
): Promise<MerchantTransactionsListDTO> => {
9293
const result = await apiClient.getMerchantTransactionsProcessed({

src/api/__tests__/MerchantsApiClient.test.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { extractResponse } from '@pagopa/selfcare-common-frontend/utils/api-utils';
22
import { createClient } from '../generated/merchants/client';
3+
import { store } from '../../redux/store';
34

45
jest.mock('@pagopa/selfcare-common-frontend/utils/storage', () => ({
56
storageTokenOps: { read: jest.fn().mockReturnValue('mocked-token') },
67
}));
78

89
jest.mock('@pagopa/selfcare-common-frontend/redux/slices/appStateSlice', () => ({
910
appStateActions: { addError: jest.fn((e) => e) },
10-
appStateReducer: (state = {}, action: any) => state,
11+
}));
12+
13+
jest.mock('../../redux/store', () => ({
14+
store: { dispatch: jest.fn() },
1115
}));
1216

1317
jest.mock('@pagopa/selfcare-common-frontend/utils/api-utils', () => ({
@@ -21,6 +25,8 @@ jest.mock('../generated/merchants/client', () => ({
2125

2226
let mockApiClient: any;
2327

28+
store.dispatch = jest.fn();
29+
2430
describe('MerchantApi', () => {
2531
beforeEach(() => {
2632
mockApiClient = {
@@ -35,6 +41,7 @@ describe('MerchantApi', () => {
3541
putPointOfSales: jest.fn(),
3642
getPointOfSales: jest.fn(),
3743
getPointOfSale: jest.fn(),
44+
getPointOfSalesWithTransactions: jest.fn(),
3845
getPointOfSaleTransactionsProcessed: jest.fn(),
3946
downloadInvoiceFile: jest.fn(),
4047
getReportedUser: jest.fn(),
@@ -43,6 +50,7 @@ describe('MerchantApi', () => {
4350
sendRewardBatches: jest.fn(),
4451
postponeTransaction: jest.fn(),
4552
approveDownloadRewardBatch: jest.fn(),
53+
getAllRewardBatches: jest.fn()
4654
};
4755

4856
(createClient as jest.Mock).mockReturnValue(mockApiClient);
@@ -272,6 +280,42 @@ describe('MerchantApi', () => {
272280
expect(result).toBe('extracted');
273281
});
274282

283+
it('getMerchantPointOfSalesWithTransactions resolved', async () => {
284+
const json = jest.fn().mockResolvedValue("test");
285+
global.fetch = jest.fn().mockResolvedValue({ok: true, json});
286+
287+
const MerchantApi = loadApi();
288+
289+
const resolvedResult = await MerchantApi.getMerchantPointOfSalesWithTransactions('batch-id');
290+
291+
expect(global.fetch).toHaveBeenCalledWith(expect.any(String), {
292+
method: 'GET',
293+
headers: {
294+
Authorization: `Bearer mocked-token`,
295+
Accept: 'application/json',
296+
},
297+
});
298+
expect(json).toHaveBeenCalled()
299+
expect(resolvedResult).toBe("test")
300+
});
301+
302+
it('getMerchantPointOfSalesWithTransactions rejected', async () => {
303+
global.fetch = jest.fn().mockResolvedValue({ok: false});
304+
305+
const MerchantApi = loadApi();
306+
307+
const rejectedResult = await MerchantApi.getMerchantPointOfSalesWithTransactions('batch-id');
308+
309+
expect(global.fetch).toHaveBeenCalledWith(expect.any(String), {
310+
method: 'GET',
311+
headers: {
312+
Authorization: `Bearer mocked-token`,
313+
Accept: 'application/json',
314+
},
315+
});
316+
expect(rejectedResult).toStrictEqual([])
317+
});
318+
275319
it('getMerchantPointOfSaleTransactionsProcessed', async () => {
276320
mockApiClient.getPointOfSaleTransactionsProcessed.mockResolvedValue({ right: 'data' });
277321
const MerchantApi = loadApi();
@@ -437,17 +481,15 @@ describe('MerchantApi', () => {
437481
expect(result).toBe('extracted');
438482
});
439483

440-
it.skip('sendRewardBatches - error with REWARD_BATCH_PREVIOUS_NOT_SENT', async () => {
484+
it('sendRewardBatches - error with REWARD_BATCH_PREVIOUS_NOT_SENT', async () => {
441485
mockApiClient.sendRewardBatches.mockResolvedValue({
442-
left: [{ value: 'REWARD_BATCH_PREVIOUS_NOT_SENT' }],
486+
right: { value: {code: 'REWARD_BATCH_PREVIOUS_NOT_SENT'}, status: 400 },
443487
});
444488
const MerchantApi = loadApi();
445489

446490
const result = await MerchantApi.sendRewardBatches('init1', 'batch1');
447491

448-
expect(result).toEqual({
449-
code: 'REWARD_BATCH_PREVIOUS_NOT_SENT',
450-
});
492+
expect(result).toBe('REWARD_BATCH_PREVIOUS_NOT_SENT');
451493
});
452494

453495
it('sendRewardBatches - error with other code', async () => {

src/components/Footer/__tests__/Footer.test.tsx

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,48 @@
11
import React from 'react';
2-
import { render, screen, act, fireEvent } from '@testing-library/react';
2+
import { render, screen, act } from '@testing-library/react';
33
import Footer from '../Footer';
44
import { LangCode } from '@pagopa/mui-italia';
55
import { pagoPALink } from '../FooterConfig';
6+
import { ReactI18NextChild } from 'react-i18next';
67

78
let mockedMuiFooterProps: any;
89

910
const mockedPagoPALink = { ...pagoPALink };
1011

1112
jest.mock('@pagopa/mui-italia/dist/components/Footer/Footer', () => ({
12-
Footer: (props) => {
13+
Footer: (props: {
14+
loggedUser: {
15+
toString: () =>
16+
| boolean
17+
| React.ReactChild
18+
| React.ReactFragment
19+
| React.ReactPortal
20+
| Iterable<ReactI18NextChild>
21+
| null
22+
| undefined;
23+
};
24+
preLoginLinks: any;
25+
postLoginLinks: any;
26+
companyLink: any;
27+
onLanguageChanged: (arg0: string) => void;
28+
onExit: (arg0: () => void) => void;
29+
currentLangCode:
30+
| boolean
31+
| React.ReactChild
32+
| React.ReactFragment
33+
| React.ReactPortal
34+
| Iterable<ReactI18NextChild>
35+
| null
36+
| undefined;
37+
productsJsonUrl:
38+
| boolean
39+
| React.ReactChild
40+
| React.ReactFragment
41+
| React.ReactPortal
42+
| Iterable<ReactI18NextChild>
43+
| null
44+
| undefined;
45+
}) => {
1346
mockedMuiFooterProps = props;
1447
return (
1548
<div data-testid="mui-italia-footer-mock">
@@ -39,7 +72,7 @@ jest.mock('react-i18next', () => ({
3972
useTranslation: () => ({
4073
t: (key: string) => key,
4174
}),
42-
Trans: ({ i18nKey, children }: { i18nKey: string; children: React.ReactNode }) => (
75+
Trans: ({ children }: { i18nKey: string; children: React.ReactNode }) => (
4376
<div data-testid="trans-component">{children}</div>
4477
),
4578
}));
@@ -271,6 +304,30 @@ describe('<Footer />', () => {
271304
expect(window.open).toHaveBeenCalledWith('#workwithus');
272305
});
273306

307+
test('should call onClickNavigate when certifications link is clicked', () => {
308+
jest.spyOn(window, 'open');
309+
render(<Footer loggedUser={false} />);
310+
311+
const certificationsLink = mockedMuiFooterProps.preLoginLinks.resources.links.find(
312+
(link: any) => link.label === 'common.footer.preLoginLinks.resources.links.certifications'
313+
);
314+
certificationsLink?.onClick();
315+
316+
expect(window.open).toHaveBeenCalledWith('#certifications');
317+
});
318+
319+
test('should call onClickNavigate when information security link is clicked', () => {
320+
jest.spyOn(window, 'open');
321+
render(<Footer loggedUser={false} />);
322+
323+
const infoSecurityLink = mockedMuiFooterProps.preLoginLinks.resources.links.find(
324+
(link: any) => link.label === 'common.footer.preLoginLinks.resources.links.informationsecurity'
325+
);
326+
infoSecurityLink?.onClick();
327+
328+
expect(window.open).toHaveBeenCalledWith('#infosecurity');
329+
});
330+
274331
test('should have correct aria labels for all pre-login links', () => {
275332
render(<Footer loggedUser={false} />);
276333

@@ -298,9 +355,7 @@ describe('<Footer />', () => {
298355

299356
test('should render legal info with Trans component', () => {
300357
render(<Footer loggedUser={false} />);
301-
// Verify legalInfo contains the expected content structure
302358
expect(mockedMuiFooterProps.legalInfo).toBeDefined();
303-
// The Trans component wraps the legal info content
304359
expect(mockedMuiFooterProps.legalInfo.props.i18nKey).toBe('common.footer.legalInfoText');
305360
});
306361

@@ -345,6 +400,26 @@ describe('<Footer />', () => {
345400
expect(postLoginLinks[3].label).toBe('common.footer.postLoginLinks.accessibility');
346401
});
347402

403+
test('should call onClickNavigate for post-login privacy link', () => {
404+
jest.spyOn(window, 'open');
405+
render(<Footer loggedUser={true} />);
406+
407+
const privacyLink = mockedMuiFooterProps.postLoginLinks[0];
408+
privacyLink.onClick();
409+
410+
expect(window.open).toHaveBeenCalledWith(undefined);
411+
});
412+
413+
test('should call onClickNavigate for post-login terms link', () => {
414+
jest.spyOn(window, 'open');
415+
render(<Footer loggedUser={true} />);
416+
417+
const termsLink = mockedMuiFooterProps.postLoginLinks[2];
418+
termsLink.onClick();
419+
420+
expect(window.open).toHaveBeenCalledWith(undefined);
421+
});
422+
348423
test('should call onClickNavigate for post-login protection link', () => {
349424
jest.spyOn(window, 'open');
350425
render(<Footer loggedUser={true} />);
@@ -424,4 +499,11 @@ describe('<Footer />', () => {
424499
expect(social.ariaLabel).toBeDefined();
425500
});
426501
});
427-
});
502+
503+
test('should apply pagoPALink href to logo button', () => {
504+
render(<Footer loggedUser={false} />);
505+
506+
const logoButton = screen.getByTestId('logo-btn');
507+
expect(logoButton).toHaveAttribute('href', 'https://www.pagopa.it');
508+
});
509+
});

0 commit comments

Comments
 (0)