Impl P2P Bank Account Destination (#279)

This commit is contained in:
Ildar Galeev 2024-03-05 01:03:12 +07:00 committed by GitHub
parent bb46e6f4fd
commit 7cba00afb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 246 additions and 13 deletions

View File

View 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)}`);
}
};

View 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)}`);
}
};

View 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)}`);
}
};

View 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';

View 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;

View 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)}`,
);
});
});

View 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;
}

View File

@ -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';

View File

@ -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';

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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 = {

View File

@ -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 = {

View File

@ -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;

View File

@ -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' }

View File

@ -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' }

View File

@ -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' }

View File

@ -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:",

View File

@ -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": "পরিমাণ:",

View File

@ -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:",

View File

@ -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": "金額:",

View File

@ -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": "금액:",

View File

@ -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:",

View File

@ -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": "Сумма:",

View File

@ -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:",