Skip to content

Feat/manager 18805 #18604

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: feat/pci-project
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/manager/apps/pci-project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,4 @@
"universes": [
"@ovh-ux/manager-public-cloud"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"pci_project_new_payment_credit_explain": "Jedes Public Cloud Projekt verfügt über ein eigenes Guthabenkonto. Wenn das Guthaben nicht ausreicht, begleichen Sie den ausstehenden Betrag so schnell wie möglich mit einem eingetragenen Zahlungsmittel.",
"pci_project_new_payment_credit_info": "*Das Guthaben kann nicht übertragen oder rückerstattet werden und hat keinerlei Geldwert. Jedes Guthaben, das 13 Monate nach Erwerb nicht genutzt wird, verfällt. Wenn Sie auf „Guthaben aufladen und mein Projekt erstellen“ klicken, werden Sie auf einen Bestellschein geleitet, um Ihr Guthaben mit Kreditkarte oder per PayPal aufzuladen.",
"pci_project_new_payment_credit_amount_other": "Anderer Betrag",
"pci_project_new_payment_credit_amount_other_label": "Aufzuladender Betrag"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"pci_project_new_payment_credit_explain": "Every Public Cloud project has its own individual credit account. If there is not enough credit in your account, you will need to settle the remaining amount as soon as possible with a payment method registered to your account.",
"pci_project_new_payment_credit_info": "*Credits cannot be transferred or refunded, and do not have any monetary value. Any credit that has not been used within 13 months of purchase will be lost. Click “Credit and create my project”, and you will be redirected to an invoice to pay for your credits via payment card or PayPal.",
"pci_project_new_payment_credit_amount_other": "Other amount",
"pci_project_new_payment_credit_amount_other_label": "Amount to credit"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"pci_project_new_payment_credit_explain": "Cada proyecto de Public Cloud tiene una cuenta de créditos propia. Si la cantidad de créditos es insuficiente, deberá abonar lo antes posible el importe restante con una forma de pago memorizada en su cuenta.",
"pci_project_new_payment_credit_info": "* Los créditos no son transferibles ni reembolsables, y no tienen ningún valor monetario. Si no se utilizan en un plazo de 13 meses, se pierden. Al hacer clic en «Recargar y crear mi proyecto», será redirigido a la orden de pedido, desde donde podrá abonar los créditos por tarjeta bancaria o PayPal.",
"pci_project_new_payment_credit_amount_other": "Otro importe",
"pci_project_new_payment_credit_amount_other_label": "Importe a recargar"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"pci_project_new_payment_credit_explain": "Chaque projet Public Cloud possède un compte de crédits qui lui est propre. Si le montant du crédit est insuffisant, vous devrez régler la somme restant due avec un moyen de paiement enregistré dans les plus brefs délais.",
"pci_project_new_payment_credit_info": "* Les crédits ne sont pas transférables ni remboursables et ne possèdent aucune valeur monétaire. Tout crédit non utilisé dans les 13 mois suivant leur achat sont perdus. En cliquant sur « Créditer et créer mon projet », vous allez être redirigé vers un bon de commande pour régler vos crédits par carte bancaire ou Paypal.",
"pci_project_new_payment_credit_amount_other": "Autre montant",
"pci_project_new_payment_credit_amount_other_label": "Montant à créditer"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"pci_project_new_payment_credit_explain": "Chaque projet Public Cloud possède un compte de crédits qui lui est propre. Si le montant du crédit est insuffisant, vous devrez régler la somme restant due avec un moyen de paiement enregistré dans les plus brefs délais.",
"pci_project_new_payment_credit_info": "* Les crédits ne sont pas transférables ni remboursables et ne possèdent aucune valeur monétaire. Tout crédit non utilisé dans les 13 mois suivant leur achat sont perdus. En cliquant sur « Créditer et créer mon projet », vous allez être redirigé vers un bon de commande pour régler vos crédits par carte bancaire ou Paypal.",
"pci_project_new_payment_credit_amount_other": "Autre montant",
"pci_project_new_payment_credit_amount_other_label": "Montant à créditer"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"pci_project_new_payment_credit_explain": "Ogni progetto Public cloud dispone di un conto di crediti dedicato. Se il credito non è sufficiente per pagare l’intero importo, sarà necessario saldare la differenza nel più breve tempo possibile, utilizzando uno dei metodi di pagamenti registrati.",
"pci_project_new_payment_credit_info": "*Il credito non ha valore monetario e non è trasferibile o rimborsabile. Gli importi non utilizzati nei 13 mesi successivi all’accredito vengono persi. Cliccando su “Ricarica e crea il tuo progetto” verrai reindirizzato al buono d’ordine per effettuare l’accredito tramite carta bancaria o PayPal.",
"pci_project_new_payment_credit_amount_other": "Altro importo",
"pci_project_new_payment_credit_amount_other_label": "Importo da accreditare"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"pci_project_new_payment_credit_explain": "Do każdego projektu Public Cloud przypisane jest oddzielne konto przedpłacone. Jeśli kwota zasilenia jest niewystarczająca, ureguluj jak najszybciej pozostałą należną kwotę, używając zarejestrowanego sposobu płatności.",
"pci_project_new_payment_credit_info": "* Środki z zasileń nie mogą być przenoszone między kontami, nie podlegają zwrotom i nie posiadają wartości pieniężnej. Zasilenie niewykorzystane w ciągu 13 miesięcy od zakupu traci swoją ważność. Po kliknięciu na „Zasil i utwórz projekt” zostaniesz przekierowany do zamówienia, abyś mógł uregulować należność za pomocą karty kredytowej lub PayPal.",
"pci_project_new_payment_credit_amount_other": "Inna kwota",
"pci_project_new_payment_credit_amount_other_label": "Kwota zasilenia"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"pci_project_new_payment_credit_explain": "Cada projeto Public Cloud dispõe de uma própria conta de créditos. Se o montante do crédito for insuficiente, deverá pagar o montante restante devido através de um método de pagamento registado o mais rápido possível.",
"pci_project_new_payment_credit_info": "* Os créditos não podem ser transferidos nem são reembolsáveis, e não possuem qualquer valor monetário. Qualquer crédito não utilizado nos 13 meses a seguir à compra será perdido. Ao clicar em “Creditar e criar o meu projeto”, vai ser redirecionado para um voucher para pagar os seus créditos por cartão de crédito ou PayPal.",
"pci_project_new_payment_credit_amount_other": "Outro montante",
"pci_project_new_payment_credit_amount_other_label": "Montante a creditar"
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
TPaymentMethodType,
TUserPaymentMethod,
} from '@/data/types/payment/payment-method.type';
import { CHALLENGE_CREDIT_CARD_LENGTH } from '@/payment/constants';

const PAYMENT_TYPES_TO_SUBMIT_IN_CHALLENGE = [
TPaymentMethodType.CREDIT_CARD,
Expand Down Expand Up @@ -67,7 +68,10 @@ const PaymentMethodChallenge: React.FC<TPaymentMethodChallengeProps> = ({
const isValidValue = () => {
switch (paymentMethod?.paymentType) {
case TPaymentMethodType.CREDIT_CARD:
return value.length === 6 && /^[0-9]+$/.test(value);
return (
value.length === CHALLENGE_CREDIT_CARD_LENGTH &&
/^[0-9]+$/.test(value)
);
case TPaymentMethodType.BANK_ACCOUNT:
return value.length > 0 && isIBAN(value);
default:
Expand Down Expand Up @@ -170,7 +174,7 @@ const PaymentMethodChallenge: React.FC<TPaymentMethodChallengeProps> = ({
leading-[1.29] tracking-[1.86px]
text-[var(--ods-color-text)] top-[68px] left-[10px]"
placeholder="XXXX XX"
maxLength={6}
maxLength={CHALLENGE_CREDIT_CARD_LENGTH}
value={value}
onChange={(e) => setValue(e.target.value)}
aria-label={t('pci_project_new_payment_challenge_credit_card')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ describe('PaymentMethods', () => {
});

describe('Payment Method Challenge', () => {
it('should render PaymentMethodChallenge when handlePaymentMethodChallenge is true', () => {
it('should render PaymentMethodChallenge', () => {
const mockEligibility = createMockEligibility({
actionsRequired: ['challengePaymentMethod'],
});
Expand All @@ -523,9 +523,7 @@ describe('PaymentMethods', () => {

render(
<Wrapper>
<PaymentMethods
{...createMockProps({ handlePaymentMethodChallenge: true })}
/>
<PaymentMethods {...createMockProps()} />
</Wrapper>,
);

Expand All @@ -534,37 +532,6 @@ describe('PaymentMethods', () => {
expect(screen.getByDisplayValue('')).toBeInTheDocument(); // The challenge input
});

it('should not render PaymentMethodChallenge when handlePaymentMethodChallenge is false', () => {
const mockEligibility = createMockEligibility({
actionsRequired: ['challengePaymentMethod'],
});
const mockPaymentMethod = createMockUserPaymentMethod({
paymentType: TPaymentMethodType.CREDIT_CARD,
default: true,
});

mockUseEligibility.mockReturnValue(
createMockEligibilityResult(mockEligibility),
);
mockUsePaymentMethods.mockImplementation((params) => {
if (params?.default) {
return createMockPaymentMethodsResult({ data: [mockPaymentMethod] });
}
return createMockPaymentMethodsResult({ data: [mockPaymentMethod] });
});

render(
<Wrapper>
<PaymentMethods
{...createMockProps({ handlePaymentMethodChallenge: false })}
/>
</Wrapper>,
);

// Challenge component should not be rendered when disabled
expect(screen.queryByPlaceholderText('XXXX XX')).not.toBeInTheDocument();
});

it('should handle challenge validity changes and call handleValidityChange', async () => {
const user = userEvent.setup();
const mockHandleValidityChange = vi.fn();
Expand All @@ -591,7 +558,6 @@ describe('PaymentMethods', () => {
<PaymentMethods
{...createMockProps({
handleValidityChange: mockHandleValidityChange,
handlePaymentMethodChallenge: true,
})}
/>
</Wrapper>,
Expand Down Expand Up @@ -634,7 +600,6 @@ describe('PaymentMethods', () => {
<PaymentMethods
{...createMockProps({
handleValidityChange: mockHandleValidityChange,
handlePaymentMethodChallenge: true,
})}
/>
</Wrapper>,
Expand Down Expand Up @@ -664,7 +629,6 @@ describe('PaymentMethods', () => {
<PaymentMethods
{...createMockProps({
paymentMethodHandler: paymentMethodRef,
handlePaymentMethodChallenge: true,
})}
/>
</Wrapper>,
Expand Down Expand Up @@ -714,7 +678,6 @@ describe('PaymentMethods', () => {
<PaymentMethods
{...createMockProps({
paymentMethodHandler: paymentMethodRef,
handlePaymentMethodChallenge: true,
})}
/>
</Wrapper>,
Expand Down Expand Up @@ -771,7 +734,6 @@ describe('PaymentMethods', () => {
<PaymentMethods
{...createMockProps({
paymentMethodHandler: paymentMethodRef,
handlePaymentMethodChallenge: true,
})}
/>
</Wrapper>,
Expand All @@ -798,41 +760,6 @@ describe('PaymentMethods', () => {
expect(mockQueryClient.invalidateQueries).toHaveBeenCalledTimes(2);
});

it('should resolve immediately when handlePaymentMethodChallenge is false', async () => {
const mockEligibility = createMockEligibility();
const mockPaymentMethod = createMockUserPaymentMethod({ default: true });
const paymentMethodRef = React.createRef<TPaymentMethodRef>();

mockUseEligibility.mockReturnValue(
createMockEligibilityResult(mockEligibility),
);
mockUsePaymentMethods.mockReturnValue(
createMockPaymentMethodsResult({ data: [mockPaymentMethod] }),
);

render(
<Wrapper>
<PaymentMethods
{...createMockProps({
paymentMethodHandler: paymentMethodRef,
handlePaymentMethodChallenge: false,
})}
/>
</Wrapper>,
);

await waitFor(() => {
expect(paymentMethodRef.current).toBeTruthy();
});

if (!paymentMethodRef.current) {
throw new Error('Payment method ref is null');
}

const result = await paymentMethodRef.current.submitPaymentMethod();
expect(result).toBe(true);
});

it('should handle submission when no payment method challenge handler exists', async () => {
const mockEligibility = createMockEligibility();
const mockPaymentMethod = createMockUserPaymentMethod({ default: true });
Expand All @@ -850,7 +777,6 @@ describe('PaymentMethods', () => {
<PaymentMethods
{...createMockProps({
paymentMethodHandler: paymentMethodRef,
handlePaymentMethodChallenge: true,
})}
/>
</Wrapper>,
Expand Down Expand Up @@ -888,7 +814,6 @@ describe('PaymentMethods', () => {
<PaymentMethods
{...createMockProps({
handleValidityChange: mockHandleValidityChange,
handlePaymentMethodChallenge: true,
})}
/>
</Wrapper>,
Expand Down Expand Up @@ -916,7 +841,6 @@ describe('PaymentMethods', () => {
<PaymentMethods
{...createMockProps({
handleValidityChange: mockHandleValidityChange,
handlePaymentMethodChallenge: true,
})}
/>
</Wrapper>,
Expand All @@ -927,7 +851,7 @@ describe('PaymentMethods', () => {
});
});

it('should be valid when challenge is disabled and payment method conditions are met', async () => {
it('should be valid when payment method conditions are met', async () => {
const mockHandleValidityChange = vi.fn();
const mockEligibility = createMockEligibility();
const mockPaymentMethod = createMockUserPaymentMethod({ default: true });
Expand All @@ -944,7 +868,6 @@ describe('PaymentMethods', () => {
<PaymentMethods
{...createMockProps({
handleValidityChange: mockHandleValidityChange,
handlePaymentMethodChallenge: false,
})}
/>
</Wrapper>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import PaymentMethodChallenge, {
TPaymentMethodChallengeRef,
} from './PaymentMethodChallenge';
import queryClient from '@/queryClient';
import PaymentMethodIntegration from './integrations/PaymentMethodIntegration';

export type TPaymentMethodError =
| 'challenge_retry'
Expand All @@ -25,7 +26,6 @@ export type PaymentMethodsProps = {
paymentMethodHandler: React.Ref<TPaymentMethodRef>;
handlePaymentMethodChange?: (method: TPaymentMethod) => void;
handleSetAsDefaultChange?: (value: boolean) => void;
handlePaymentMethodChallenge?: boolean;
handleValidityChange?: (isValid: boolean) => void;
};

Expand Down Expand Up @@ -81,7 +81,6 @@ export type TPaymentMethodRef = {
* paymentMethodHandler={paymentMethodRef}
* handlePaymentMethodChange={(method) => console.log('Selected method:', method)}
* handleSetAsDefaultChange={(isDefault) => console.log('Set as default:', isDefault)}
* handlePaymentMethodChallenge={true}
* handleValidityChange={handlePaymentValidityChange}
* />
* <button onClick={handleSubmit} disabled={!isPaymentValid}>
Expand All @@ -94,15 +93,13 @@ export type TPaymentMethodRef = {
* @param props.paymentMethodHandler - Ref to access payment method submission functionality
* @param props.handlePaymentMethodChange - Optional callback when payment method selection changes
* @param props.handleSetAsDefaultChange - Optional callback when default payment method setting changes
* @param props.handlePaymentMethodChallenge - Whether to handle payment method challenges (default: true)
* @param props.handleValidityChange - Optional callback when payment method validity state changes
*
* @returns JSX element containing payment method components
*/
const PaymentMethods: React.FC<PaymentMethodsProps> = ({
handlePaymentMethodChange = () => {},
handleSetAsDefaultChange = () => {},
handlePaymentMethodChallenge = true,
handleValidityChange = () => {},
paymentMethodHandler,
}) => {
Expand All @@ -121,6 +118,10 @@ const PaymentMethods: React.FC<PaymentMethodsProps> = ({
const [isChallengeValid, setIsChallengeValid] = React.useState<boolean>(
false,
);
const [
isPaymentMethodIntegrationValid,
setIsPaymentMethodIntegrationValid,
] = React.useState<boolean>(false);
const [
selectedPaymentMethod,
setSelectedPaymentMethod,
Expand Down Expand Up @@ -149,20 +150,19 @@ const PaymentMethods: React.FC<PaymentMethodsProps> = ({
useEffect(() => {
// Here we check if everything is ok about the payment method

// The challenge has passed (or is not required)
const isPaymentChallengeValid =
!handlePaymentMethodChallenge || isChallengeValid;

// A payment method is selected and is set as default
const hasValidPaymentMethod = !!selectedPaymentMethod && isSetAsDefault;

const isValid = isPaymentChallengeValid && hasValidPaymentMethod;
const isValid =
isChallengeValid &&
hasValidPaymentMethod &&
isPaymentMethodIntegrationValid;

handleValidityChange(isValid);
}, [
isChallengeValid,
isPaymentMethodIntegrationValid,
handleValidityChange,
handlePaymentMethodChallenge,
selectedPaymentMethod,
isSetAsDefault,
]);
Expand All @@ -174,7 +174,7 @@ const PaymentMethods: React.FC<PaymentMethodsProps> = ({
() => {
return {
submitPaymentMethod: async () => {
if (handlePaymentMethodChallenge && paymentChallengeRef.current) {
if (paymentChallengeRef.current) {
const status = await paymentChallengeRef.current.submitChallenge();
switch (status) {
case 'deactivated':
Expand All @@ -199,7 +199,7 @@ const PaymentMethods: React.FC<PaymentMethodsProps> = ({
},
};
},
[handlePaymentMethodChallenge],
[paymentChallengeRef],
);

if (isLoadingDefault || isLoadingEligibility || !eligibility) {
Expand All @@ -213,17 +213,24 @@ const PaymentMethods: React.FC<PaymentMethodsProps> = ({
) : (
<RegisterPaymentMethod
eligibility={eligibility}
handlePaymentMethodChange={handlePaymentMethodChange}
handleSetAsDefaultChange={handleSetAsDefaultChange}
handlePaymentMethodChange={onHandlePaymentMethodChange}
handleSetAsDefaultChange={onHandleSetAsDefaultChange}
/>
)}
{handlePaymentMethodChallenge && (
<PaymentMethodChallenge
eligibility={eligibility}
challengeHandler={paymentChallengeRef}
paymentMethod={defaultPaymentMethod}
handleValidityChange={(isValid) => setIsChallengeValid(isValid)}
/>
{!defaultPaymentMethod && (
<div className="mb-6">
<PaymentMethodChallenge
<PaymentMethodIntegration
paymentMethod={selectedPaymentMethod}
handleValidityChange={(isValid) =>
setIsPaymentMethodIntegrationValid(isValid)
}
eligibility={eligibility}
challengeHandler={paymentChallengeRef}
paymentMethod={defaultPaymentMethod}
handleValidityChange={(isValid) => setIsChallengeValid(isValid)}
/>
</div>
)}
Expand Down
Loading
Loading