mirror of
https://github.com/valitydev/checkout.git
synced 2024-11-06 10:35:20 +00:00
Impl P2P Bank Account Destination (#279)
This commit is contained in:
parent
bb46e6f4fd
commit
7cba00afb7
0
src/common/backend/index.ts
Normal file
0
src/common/backend/index.ts
Normal file
16
src/common/backend/p2p/complete.ts
Normal file
16
src/common/backend/p2p/complete.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { extractError, fetchApi } from '../../../common/utils';
|
||||
|
||||
export type CompleteInfo = {
|
||||
invoiceId: string;
|
||||
paymentId: string;
|
||||
payerTransactionId?: string;
|
||||
};
|
||||
|
||||
export const complete = async (capiEndpoint: string, accessToken: string, info: CompleteInfo): Promise<void> => {
|
||||
try {
|
||||
await fetchApi(capiEndpoint, accessToken, 'POST', 'p2p/payments/complete', info);
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch destinations: ${extractError(error)}`);
|
||||
throw new Error(`Failed to fetch destinations: ${extractError(error)}`);
|
||||
}
|
||||
};
|
24
src/common/backend/p2p/getDestinations.ts
Normal file
24
src/common/backend/p2p/getDestinations.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Destination } from './types';
|
||||
import { extractError, fetchApi } from '../../../common/utils';
|
||||
|
||||
export const getDestinations = async (
|
||||
capiEndpoint: string,
|
||||
accessToken: string,
|
||||
invoiceID: string,
|
||||
paymentID: string,
|
||||
gatewayID: string,
|
||||
): Promise<Destination[]> => {
|
||||
const queryParams = new URLSearchParams({
|
||||
invoiceId: invoiceID,
|
||||
paymentId: paymentID,
|
||||
gatewayId: gatewayID,
|
||||
}).toString();
|
||||
const path = `p2p/payments/destinations?${queryParams}`;
|
||||
try {
|
||||
const response = await fetchApi(capiEndpoint, accessToken, 'GET', path);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch destinations: ${extractError(error)}`);
|
||||
throw new Error(`Failed to fetch destinations: ${extractError(error)}`);
|
||||
}
|
||||
};
|
22
src/common/backend/p2p/getGateways.ts
Normal file
22
src/common/backend/p2p/getGateways.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Gateway } from './types';
|
||||
import { extractError, fetchApi } from '../../../common/utils';
|
||||
|
||||
export const getGateways = async (
|
||||
capiEndpoint: string,
|
||||
accessToken: string,
|
||||
invoiceID: string,
|
||||
paymentID: string,
|
||||
): Promise<Gateway[]> => {
|
||||
const queryParams = new URLSearchParams({
|
||||
invoiceId: invoiceID,
|
||||
paymentId: paymentID,
|
||||
}).toString();
|
||||
const path = `p2p/payments/gateways?${queryParams}`;
|
||||
try {
|
||||
const response = await fetchApi(capiEndpoint, accessToken, 'GET', path);
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch gateways: ${extractError(error)}`);
|
||||
throw new Error(`Failed to fetch gateways: ${extractError(error)}`);
|
||||
}
|
||||
};
|
5
src/common/backend/p2p/index.ts
Normal file
5
src/common/backend/p2p/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { complete } from './complete';
|
||||
export { getDestinations } from './getDestinations';
|
||||
export { getGateways } from './getGateways';
|
||||
|
||||
export type { Gateway, Destination, DestinationBankAccount, DestinationBankCard, DestinationSBP } from './types';
|
26
src/common/backend/p2p/types.ts
Normal file
26
src/common/backend/p2p/types.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export type Gateway = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type DestinationBankCard = {
|
||||
destinationType: 'BankCard';
|
||||
pan: string;
|
||||
bankName?: string;
|
||||
};
|
||||
|
||||
export type DestinationSBP = {
|
||||
destinationType: 'DestinationSBP';
|
||||
phoneNumber: string;
|
||||
bankName?: string;
|
||||
recipientName?: string;
|
||||
};
|
||||
|
||||
export type DestinationBankAccount = {
|
||||
destinationType: 'BankAccount';
|
||||
account: string;
|
||||
bankName?: string;
|
||||
recipientName?: string;
|
||||
};
|
||||
|
||||
export type Destination = DestinationBankCard | DestinationSBP | DestinationBankAccount;
|
45
src/common/utils/fetchApi.test.ts
Normal file
45
src/common/utils/fetchApi.test.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { fetchApi } from './fetchApi';
|
||||
|
||||
beforeEach(() => {
|
||||
global.fetch = jest.fn();
|
||||
});
|
||||
|
||||
describe('fetchApi', () => {
|
||||
it('handles successful responses correctly', async () => {
|
||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ data: 'success' }),
|
||||
});
|
||||
|
||||
const response = await fetchApi('https://api.example.com', 'token123', 'GET', 'path');
|
||||
const data = await response.json();
|
||||
expect(data).toEqual({ data: 'success' });
|
||||
});
|
||||
|
||||
it('throws a generic error for non-JSON error responses', async () => {
|
||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
// Simulating a failure in response.json() method
|
||||
json: () => Promise.reject(new Error('Failed to parse JSON')),
|
||||
});
|
||||
|
||||
await expect(fetchApi('https://api.example.com', 'token123', 'POST', 'path', {})).rejects.toThrow(
|
||||
`API error: 500 Endpoint: https://api.example.com/path`,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error with JSON error details for 400 responses', async () => {
|
||||
const errorDetails = { error: 'Bad Request', message: 'Invalid parameters' };
|
||||
|
||||
(global.fetch as jest.Mock).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
json: () => Promise.resolve({ error: 'Bad Request', message: 'Invalid parameters' }),
|
||||
});
|
||||
|
||||
await expect(fetchApi('https://api.example.com', 'token123', 'POST', 'path', {})).rejects.toThrow(
|
||||
`API error: 400 Endpoint: https://api.example.com/path ${JSON.stringify(errorDetails)}`,
|
||||
);
|
||||
});
|
||||
});
|
32
src/common/utils/fetchApi.ts
Normal file
32
src/common/utils/fetchApi.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import guid from 'checkout/utils/guid';
|
||||
|
||||
export async function fetchApi(
|
||||
endpoint: string,
|
||||
accessToken: string,
|
||||
method: string,
|
||||
path: string,
|
||||
body?: any,
|
||||
): Promise<Response> {
|
||||
const response = await fetch(`${endpoint}/${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'X-Request-ID': guid(),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorDetails = `API error: ${response.status} Endpoint: ${endpoint}/${path}`;
|
||||
try {
|
||||
const errorBody = await response.json();
|
||||
errorDetails += ` ${JSON.stringify(errorBody)}`;
|
||||
} catch (ex) {
|
||||
// Ignore error
|
||||
}
|
||||
throw new Error(errorDetails);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
@ -15,6 +15,7 @@ export { isString } from './isString';
|
||||
export { getEncodedUrlParams } from './getEncodedUrlParams';
|
||||
export { findMetadata } from './findMetadata';
|
||||
export { extractError } from './extractError';
|
||||
export { fetchApi } from './fetchApi';
|
||||
|
||||
export type { CountrySubdivision, Country } from './countries';
|
||||
export { countries } from './countries';
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Gateway } from 'checkout/backend';
|
||||
|
||||
import { CompletePayment } from './CompletePayment';
|
||||
import { Destinations } from './Destinations';
|
||||
import { GatewaySelector } from './GatewaySelector';
|
||||
import { Gateway } from '../../../common/backend/p2p';
|
||||
import { LocaleContext, PaymentConditionsContext, PaymentContext, PaymentModelContext } from '../../../common/contexts';
|
||||
import { InvoiceDetermined, PaymentStarted } from '../../../common/paymentCondition';
|
||||
import { isNil } from '../../../common/utils';
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { Destination } from 'checkout/backend';
|
||||
import { Locale } from 'checkout/locale';
|
||||
|
||||
import { DestinationInfoBankAccount } from './DestinationInfoBankAccount';
|
||||
import { DestinationInfoBankCard } from './DestinationInfoBankCard';
|
||||
import { DestinationInfoSpb } from './DestinationInfoSpb';
|
||||
import { Destination } from '../../../../common/backend/p2p';
|
||||
import { ViewModelContext } from '../../../../common/contexts';
|
||||
import { Info, Container, Row, Label, Value, Alert } from '../commonComponents';
|
||||
|
||||
@ -37,6 +38,9 @@ export const DestinationInfo = ({ locale, destination }: DestinationInfoProps) =
|
||||
{destination.destinationType === 'DestinationSBP' && (
|
||||
<DestinationInfoSpb destination={destination} locale={locale} />
|
||||
)}
|
||||
{destination.destinationType === 'BankAccount' && (
|
||||
<DestinationInfoBankAccount destination={destination} locale={locale} />
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,34 @@
|
||||
import { Locale } from 'checkout/locale';
|
||||
|
||||
import { CopyToClipboard } from './CopyToClipboard';
|
||||
import { DestinationBankAccount } from '../../../../common/backend/p2p';
|
||||
import { Container, Label, Row, Value } from '../commonComponents';
|
||||
|
||||
export type DestinationInfoBankCardInfo = {
|
||||
locale: Locale;
|
||||
destination: DestinationBankAccount;
|
||||
};
|
||||
|
||||
export const DestinationInfoBankAccount = ({ locale, destination }: DestinationInfoBankCardInfo) => (
|
||||
<Container>
|
||||
<Row>
|
||||
<Label>{locale['form.p2p.destination.bank.account.account']}</Label>
|
||||
<Row $gap={8}>
|
||||
<Value>{destination.account}</Value>
|
||||
<CopyToClipboard copyValue={destination.account} />
|
||||
</Row>
|
||||
</Row>
|
||||
{destination?.bankName && (
|
||||
<Row>
|
||||
<Label>{locale['form.p2p.destination.bank.account.bank']}</Label>
|
||||
<Value>{destination.bankName}</Value>
|
||||
</Row>
|
||||
)}
|
||||
{destination?.recipientName && (
|
||||
<Row>
|
||||
<Label>{locale['form.p2p.destination.bank.account.recipient']}</Label>
|
||||
<Value>{destination.recipientName}</Value>
|
||||
</Row>
|
||||
)}
|
||||
</Container>
|
||||
);
|
@ -1,7 +1,7 @@
|
||||
import { DestinationBankCard } from 'checkout/backend/p2p';
|
||||
import { Locale } from 'checkout/locale';
|
||||
|
||||
import { CopyToClipboard } from './CopyToClipboard';
|
||||
import { DestinationBankCard } from '../../../../common/backend/p2p';
|
||||
import { Container, Label, Row, Value } from '../commonComponents';
|
||||
|
||||
export type DestinationInfoBankCardInfo = {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { DestinationSBP } from 'checkout/backend';
|
||||
import { Locale } from 'checkout/locale';
|
||||
|
||||
import { CopyToClipboard } from './CopyToClipboard';
|
||||
import { DestinationSBP } from '../../../../common/backend/p2p';
|
||||
import { Container, Label, Row, Value } from '../commonComponents';
|
||||
|
||||
export type DestinationInfoSpbProps = {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Gateway } from 'checkout/backend';
|
||||
import { Select } from 'checkout/components/ui';
|
||||
import { useGateways } from 'checkout/hooks/p2p';
|
||||
import { Locale } from 'checkout/locale';
|
||||
|
||||
import { useGateways } from './useGateways';
|
||||
import { Gateway } from '../../../common/backend/p2p';
|
||||
import { Select } from '../../../components/legacy';
|
||||
|
||||
export type GatewaySelectorProps = {
|
||||
locale: Locale;
|
||||
capiEndpoint: string;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useCallback, useReducer } from 'react';
|
||||
|
||||
import { complete as completeApi } from 'checkout/backend/p2p';
|
||||
import { complete as completeApi } from '../../../common/backend/p2p';
|
||||
|
||||
type State =
|
||||
| { status: 'PRISTINE' }
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useCallback, useReducer, useRef } from 'react';
|
||||
|
||||
import { Destination, getDestinations as getApiDestinations } from 'checkout/backend';
|
||||
import { Destination, getDestinations as getApiDestinations } from '../../../common/backend/p2p';
|
||||
|
||||
type State =
|
||||
| { status: 'PRISTINE' }
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useCallback, useReducer } from 'react';
|
||||
|
||||
import { Gateway, getGateways as getApiGateways } from 'checkout/backend';
|
||||
import { Gateway, getGateways as getApiGateways } from '../../../common/backend/p2p';
|
||||
|
||||
type State =
|
||||
| { status: 'PRISTINE' }
|
||||
|
@ -45,7 +45,7 @@
|
||||
"form.pay.paymentTerminalBankCard.providerSelectorDescription": "Ödəniş sistemini seçin:",
|
||||
"form.qr.code": "Ödənişi tamamlamaq üçün bank tətbiqinizlə və ya telefon kamera ilə QR kodunu skan edin",
|
||||
"form.p2p.loading": "Yüklənir...",
|
||||
"form.p2p.error": "Server xətası baş verdi",
|
||||
"form.p2p.error": "Fərqli bir məbləğ üçün bir ərizə yaradın və ya daha sonra təkrarlamağa çalışın",
|
||||
"form.p2p.select.destination": "Təyinat yeri seçin...",
|
||||
"form.p2p.destination.info": "Sifariş ödənişi:",
|
||||
"form.p2p.destination.bank.card.pan": "Bu karta:",
|
||||
@ -53,6 +53,9 @@
|
||||
"form.p2p.destination.spb.bank.name": "Bankın adı:",
|
||||
"form.p2p.destination.spb.phone": "Telefon nömrəsi:",
|
||||
"form.p2p.destination.spb.recipient": "Adı:",
|
||||
"form.p2p.destination.bank.account.account": "Hesab nömrəsi:",
|
||||
"form.p2p.destination.bank.account.bank": "Bankın adı:",
|
||||
"form.p2p.destination.bank.account.recipient": "Adı:",
|
||||
"form.p2p.complete.info": "Təqdim etdikdən sonra \"Köçürmə tamamlandı\" düyməsini basın. Ödənişin işlənməsi 5 dəqiqəyə qədər çəkir.",
|
||||
"form.p2p.complete.button": "Köçürmə tamamlandı",
|
||||
"form.p2p.destination.amount": "Köçürmə məbləği:",
|
||||
|
@ -53,6 +53,9 @@
|
||||
"form.p2p.destination.spb.bank.name": "ব্যাংকের নাম:",
|
||||
"form.p2p.destination.spb.phone": "ফোন নম্বর:",
|
||||
"form.p2p.destination.spb.recipient": "নাম:",
|
||||
"form.p2p.destination.bank.account.account": "অ্যাকাউন্ট নম্বর:",
|
||||
"form.p2p.destination.bank.account.bank": "ব্যাংকের নাম:",
|
||||
"form.p2p.destination.bank.account.recipient": "নাম:",
|
||||
"form.p2p.complete.info": "ট্রান্সফার সম্পূর্ণ হওয়ার পর, নীচের বোতাম চাপুন।",
|
||||
"form.p2p.complete.button": "পেমেন্ট সম্পন্ন করুন",
|
||||
"form.p2p.destination.amount": "পরিমাণ:",
|
||||
|
@ -53,6 +53,9 @@
|
||||
"form.p2p.destination.spb.bank.name": "Bank name:",
|
||||
"form.p2p.destination.spb.phone": "Phone number:",
|
||||
"form.p2p.destination.spb.recipient": "Name:",
|
||||
"form.p2p.destination.bank.account.account": "Account number:",
|
||||
"form.p2p.destination.bank.account.bank": "Bank name:",
|
||||
"form.p2p.destination.bank.account.recipient": "Name:",
|
||||
"form.p2p.complete.info": "After completing the transfer, press the button below.",
|
||||
"form.p2p.complete.button": "Complete payment",
|
||||
"form.p2p.destination.amount": "Amount:",
|
||||
|
@ -53,6 +53,9 @@
|
||||
"form.p2p.destination.spb.bank.name": "銀行名:",
|
||||
"form.p2p.destination.spb.phone": "電話番号:",
|
||||
"form.p2p.destination.spb.recipient": "名前:",
|
||||
"form.p2p.destination.bank.account.account": "口座番号:",
|
||||
"form.p2p.destination.bank.account.bank": "銀行名:",
|
||||
"form.p2p.destination.bank.account.recipient": "名前:",
|
||||
"form.p2p.complete.info": "送金完了後、以下のボタンを押してください。",
|
||||
"form.p2p.complete.button": "支払い完了",
|
||||
"form.p2p.destination.amount": "金額:",
|
||||
|
@ -53,6 +53,9 @@
|
||||
"form.p2p.destination.spb.bank.name": "은행 이름:",
|
||||
"form.p2p.destination.spb.phone": "전화번호:",
|
||||
"form.p2p.destination.spb.recipient": "이름:",
|
||||
"form.p2p.destination.bank.account.account": "계좌 번호:",
|
||||
"form.p2p.destination.bank.account.bank": "은행 이름:",
|
||||
"form.p2p.destination.bank.account.recipient": "이름:",
|
||||
"form.p2p.complete.info": "이체를 완료한 후 아래 버튼을 누르세요.",
|
||||
"form.p2p.complete.button": "결제 완료",
|
||||
"form.p2p.destination.amount": "금액:",
|
||||
|
@ -53,6 +53,9 @@
|
||||
"form.p2p.destination.spb.bank.name": "Nome do banco:",
|
||||
"form.p2p.destination.spb.phone": "Número de telefone:",
|
||||
"form.p2p.destination.spb.recipient": "Nome:",
|
||||
"form.p2p.destination.bank.account.account": "Número da conta:",
|
||||
"form.p2p.destination.bank.account.bank": "Nome do banco:",
|
||||
"form.p2p.destination.bank.account.recipient": "Nome:",
|
||||
"form.p2p.complete.info": "Após completar a transferência, pressione o botão abaixo.",
|
||||
"form.p2p.complete.button": "Concluir pagamento",
|
||||
"form.p2p.destination.amount": "Valor:",
|
||||
|
@ -45,7 +45,7 @@
|
||||
"form.pay.paymentTerminalBankCard.providerSelectorDescription": "Выберите платежную систему:",
|
||||
"form.qr.code": "Для оплаты отсканируйте QR-код в мобильном приложении банка или штатной камерой телефона",
|
||||
"form.p2p.loading": "Загрузка...",
|
||||
"form.p2p.error": "Произошла ошибка сервера",
|
||||
"form.p2p.error": "Создайте заявку на другую сумму или попробуйте повторить позднее",
|
||||
"form.p2p.select.destination": "Выберите банк...",
|
||||
"form.p2p.destination.info": "Осуществите перевод, используя следующие реквизиты:",
|
||||
"form.p2p.destination.bank.card.pan": "Номер карты:",
|
||||
@ -53,6 +53,9 @@
|
||||
"form.p2p.destination.spb.bank.name": "Банк:",
|
||||
"form.p2p.destination.spb.phone": "Номер телефона:",
|
||||
"form.p2p.destination.spb.recipient": "Получатель:",
|
||||
"form.p2p.destination.bank.account.account": "Номер счета:",
|
||||
"form.p2p.destination.bank.account.bank": "Банк:",
|
||||
"form.p2p.destination.bank.account.recipient": "Получатель:",
|
||||
"form.p2p.complete.info": "После совершения перевода нажмите кнопку ниже. Обработка платежа занимает до 5 минут.",
|
||||
"form.p2p.complete.button": "Перевод выполнен",
|
||||
"form.p2p.destination.amount": "Сумма:",
|
||||
|
@ -53,6 +53,9 @@
|
||||
"form.p2p.destination.spb.bank.name": "Banka adı:",
|
||||
"form.p2p.destination.spb.phone": "Telefon numarası:",
|
||||
"form.p2p.destination.spb.recipient": "İsim:",
|
||||
"form.p2p.destination.bank.account.account": "Hesap numarası:",
|
||||
"form.p2p.destination.bank.account.bank": "Banka adı:",
|
||||
"form.p2p.destination.bank.account.recipient": "Adı:",
|
||||
"form.p2p.complete.info": "Transferi tamamladıktan sonra, aşağıdaki butona basın.",
|
||||
"form.p2p.complete.button": "Ödemeyi Tamamla",
|
||||
"form.p2p.destination.amount": "Tutar:",
|
||||
|
Loading…
Reference in New Issue
Block a user