From 9500c397c7bb000918b74b6b4e84d89ebdf9889a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Grandin?= Date: Tue, 17 Feb 2026 16:32:15 +0100 Subject: [PATCH 1/3] feat(billing-summary): replace placeholder with SettingsBillingSummary component and update exports --- .../settings/billing-summary.tsx | 3 +- .../organizations/feature/src/index.ts | 1 + .../invoices-list-feature.spec.tsx | 0 .../invoices-list-feature.tsx | 157 ++++++++++ .../invoices-list.spec.tsx | 0 .../table-row-invoice.spec.tsx | 0 .../table-row-invoice/table-row-invoice.tsx | 8 +- ...anization-billing-summary-feature.spec.tsx | 0 ...page-organization-billing-summary.spec.tsx | 2 +- .../plan-selection-modal-feature.tsx} | 80 +++++- .../promo-code-modal-feature.spec.tsx | 35 +++ .../promo-code-modal-feature.tsx} | 39 ++- .../promo-code-modal.spec.tsx | 0 .../settings-billing-summary.tsx | 270 ++++++++++++++++++ .../show-usage-modal-feature.spec.tsx | 0 .../show-usage-modal-feature.tsx} | 65 ++++- .../show-usage-modal.spec.tsx | 0 .../show-usage-value-modal.spec.tsx | 0 .../show-usage-value-modal.tsx | 4 +- .../invoices-list-feature.tsx | 75 ----- ...e-organization-billing-summary-feature.tsx | 83 ------ .../plan-selection-modal-feature.tsx | 68 ----- .../promo-code-modal-feature.tsx | 38 --- .../show-usage-modal-feature.tsx | 64 ----- .../invoices-list/invoices-list.tsx | 87 ------ 25 files changed, 644 insertions(+), 435 deletions(-) rename libs/{pages/settings/src/lib/feature/page-organization-billing-summary-feature => domains/organizations/feature/src/lib/settings-billing-summary}/invoices-list-feature/invoices-list-feature.spec.tsx (100%) create mode 100644 libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/invoices-list-feature.tsx rename libs/{pages/settings/src/lib/ui/page-organization-billing-summary/invoices-list => domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature}/invoices-list.spec.tsx (100%) rename libs/{pages/settings/src/lib/ui/page-organization-billing-summary/invoices-list => domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature}/table-row-invoice/table-row-invoice.spec.tsx (100%) rename libs/{pages/settings/src/lib/ui/page-organization-billing-summary/invoices-list => domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature}/table-row-invoice/table-row-invoice.tsx (86%) rename libs/{pages/settings/src/lib/feature/page-organization-billing-summary-feature => domains/organizations/feature/src/lib/settings-billing-summary}/page-organization-billing-summary-feature.spec.tsx (100%) rename libs/{pages/settings/src/lib/ui/page-organization-billing-summary => domains/organizations/feature/src/lib/settings-billing-summary}/page-organization-billing-summary.spec.tsx (95%) rename libs/{pages/settings/src/lib/ui/page-organization-billing-summary/plan-selection-modal/plan-selection-modal.tsx => domains/organizations/feature/src/lib/settings-billing-summary/plan-selection-modal-feature/plan-selection-modal-feature.tsx} (51%) create mode 100644 libs/domains/organizations/feature/src/lib/settings-billing-summary/promo-code-modal-feature/promo-code-modal-feature.spec.tsx rename libs/{pages/settings/src/lib/ui/page-organization-billing-summary/promo-code-modal/promo-code-modal.tsx => domains/organizations/feature/src/lib/settings-billing-summary/promo-code-modal-feature/promo-code-modal-feature.tsx} (53%) rename libs/{pages/settings/src/lib/ui/page-organization-billing-summary/promo-code-modal => domains/organizations/feature/src/lib/settings-billing-summary/promo-code-modal-feature}/promo-code-modal.spec.tsx (100%) create mode 100644 libs/domains/organizations/feature/src/lib/settings-billing-summary/settings-billing-summary.tsx rename libs/{pages/settings/src/lib/feature/page-organization-billing-summary-feature => domains/organizations/feature/src/lib/settings-billing-summary}/show-usage-modal-feature/show-usage-modal-feature.spec.tsx (100%) rename libs/{pages/settings/src/lib/ui/page-organization-billing-summary/show-usage-modal/show-usage-modal.tsx => domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal-feature.tsx} (70%) rename libs/{pages/settings/src/lib/ui/page-organization-billing-summary/show-usage-modal => domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature}/show-usage-modal.spec.tsx (100%) rename libs/{pages/settings/src/lib/ui/page-organization-billing-summary => domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature}/show-usage-value-modal/show-usage-value-modal.spec.tsx (100%) rename libs/{pages/settings/src/lib/ui/page-organization-billing-summary => domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature}/show-usage-value-modal/show-usage-value-modal.tsx (92%) delete mode 100644 libs/pages/settings/src/lib/feature/page-organization-billing-summary-feature/invoices-list-feature/invoices-list-feature.tsx delete mode 100644 libs/pages/settings/src/lib/feature/page-organization-billing-summary-feature/page-organization-billing-summary-feature.tsx delete mode 100644 libs/pages/settings/src/lib/feature/page-organization-billing-summary-feature/plan-selection-modal-feature/plan-selection-modal-feature.tsx delete mode 100644 libs/pages/settings/src/lib/feature/page-organization-billing-summary-feature/promo-code-modal-feature/promo-code-modal-feature.tsx delete mode 100644 libs/pages/settings/src/lib/feature/page-organization-billing-summary-feature/show-usage-modal-feature/show-usage-modal-feature.tsx delete mode 100644 libs/pages/settings/src/lib/ui/page-organization-billing-summary/invoices-list/invoices-list.tsx diff --git a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/settings/billing-summary.tsx b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/settings/billing-summary.tsx index a98c03f6b4a..1018fc01b24 100644 --- a/apps/console-v5/src/routes/_authenticated/organization/$organizationId/settings/billing-summary.tsx +++ b/apps/console-v5/src/routes/_authenticated/organization/$organizationId/settings/billing-summary.tsx @@ -1,9 +1,10 @@ import { createFileRoute } from '@tanstack/react-router' +import { SettingsBillingSummary } from '@qovery/domains/organizations/feature' export const Route = createFileRoute('/_authenticated/organization/$organizationId/settings/billing-summary')({ component: RouteComponent, }) function RouteComponent() { - return
Hello "/_authenticated/organization/$organizationId/settings/billing-summary"!
+ return } diff --git a/libs/domains/organizations/feature/src/index.ts b/libs/domains/organizations/feature/src/index.ts index f88d67e0a66..ab67ab3d511 100644 --- a/libs/domains/organizations/feature/src/index.ts +++ b/libs/domains/organizations/feature/src/index.ts @@ -91,3 +91,4 @@ export * from './lib/free-trial-banner/free-trial-banner' export * from './lib/settings-general/settings-general' export * from './lib/settings-labels-annotations/settings-labels-annotations' export * from './lib/settings-container-registries/settings-container-registries' +export * from './lib/settings-billing-summary/settings-billing-summary' diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-summary-feature/invoices-list-feature/invoices-list-feature.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/invoices-list-feature.spec.tsx similarity index 100% rename from libs/pages/settings/src/lib/feature/page-organization-billing-summary-feature/invoices-list-feature/invoices-list-feature.spec.tsx rename to libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/invoices-list-feature.spec.tsx diff --git a/libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/invoices-list-feature.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/invoices-list-feature.tsx new file mode 100644 index 00000000000..16e44d17b6b --- /dev/null +++ b/libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/invoices-list-feature.tsx @@ -0,0 +1,157 @@ +import { useParams } from '@tanstack/react-router' +import { type Invoice } from 'qovery-typescript-axios' +import { useEffect, useState } from 'react' +import { type Value } from '@qovery/shared/interfaces' +import { InputSelectSmall, LoaderSpinner, Table, type TableFilterProps, type TableHeadProps } from '@qovery/shared/ui' +import { useInvoiceUrl } from '../../hooks/use-invoice-url/use-invoice-url' +import { useInvoices } from '../../hooks/use-invoices/use-invoices' +import TableRowInvoice from './table-row-invoice/table-row-invoice' + +export interface InvoicesListProps { + invoices?: Invoice[] + invoicesLoading?: boolean + downloadOne?: (invoiceId: string) => void + yearsForSorting?: Value[] + onFilterByYear?: (year?: string) => void + idOfInvoiceToDownload?: string +} + +export function InvoicesList(props: InvoicesListProps) { + const dataHead: TableHeadProps[] = [ + { + title: 'Date', + }, + { + title: 'Status', + filter: [ + { + title: 'Filter by status', + key: 'status', + }, + ], + }, + { + title: 'Charge', + }, + ] + const [filter, setFilter] = useState([]) + const columnWidth = '30% 30% 30% 10%' + + return ( +
+
+

Invoices

+
+ { + props.onFilterByYear && props.onFilterByYear(value) + }} + /> +
+
+ +
+ {props.invoicesLoading && ( +
+ +
+ )} + + {props.invoices?.map((invoice, index) => ( + + ))} +
+
+
+ ) +} + +export const getListOfYears = (invoices: Invoice[]) => { + const years = invoices.map((invoice) => new Date(invoice.created_at).getFullYear()) + return [...new Set(years)].sort((a, b) => b - a) +} + +export function InvoicesListFeature() { + const { organizationId = '' } = useParams({ strict: false }) + const [yearsFilterOptions, setYearsFilterOptions] = useState([]) + const [idOfInvoiceToDownload, setIdOfInvoiceToDownload] = useState(undefined) + const [invoices, setInvoices] = useState([]) + const { data: dataInvoices = [], isLoading: isLoadingDataInvoices } = useInvoices({ organizationId }) + const { mutateAsync: mutateAsyncInvoice } = useInvoiceUrl() + + const downloadOne = async (invoiceId: string) => { + if (organizationId && invoiceId) { + setIdOfInvoiceToDownload(invoiceId) + + try { + const invoiceUrl = await mutateAsyncInvoice({ organizationId, invoiceId }) + if (invoiceUrl?.url) { + const link = document.createElement('a') + link.href = invoiceUrl.url + link.download = invoiceUrl.url.substring(invoiceUrl.url.lastIndexOf('/') + 1) + link.click() + setIdOfInvoiceToDownload(undefined) + } + } catch (error) { + console.error(error) + } + } + } + + useEffect(() => { + if (dataInvoices.length > 0) { + setInvoices(dataInvoices) + const years = getListOfYears(dataInvoices) + setYearsFilterOptions([ + { label: 'All', value: '' }, + ...years.map((year) => ({ label: `${year}`, value: `${year}` })), + ]) + } + }, [dataInvoices]) + + const filterByYear = (year?: string) => { + if (invoices.length > 0) { + if (!year) return setInvoices(dataInvoices) + + const filteredInvoices = dataInvoices.filter((invoice) => { + const invoiceYear = new Date(invoice.created_at).getFullYear() + return invoiceYear === parseInt(year, 10) + }) + setInvoices(filteredInvoices) + } + } + + return ( + + ) +} + +export default InvoicesListFeature diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/invoices-list/invoices-list.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/invoices-list.spec.tsx similarity index 100% rename from libs/pages/settings/src/lib/ui/page-organization-billing-summary/invoices-list/invoices-list.spec.tsx rename to libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/invoices-list.spec.tsx diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/invoices-list/table-row-invoice/table-row-invoice.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/table-row-invoice/table-row-invoice.spec.tsx similarity index 100% rename from libs/pages/settings/src/lib/ui/page-organization-billing-summary/invoices-list/table-row-invoice/table-row-invoice.spec.tsx rename to libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/table-row-invoice/table-row-invoice.spec.tsx diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/invoices-list/table-row-invoice/table-row-invoice.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/table-row-invoice/table-row-invoice.tsx similarity index 86% rename from libs/pages/settings/src/lib/ui/page-organization-billing-summary/invoices-list/table-row-invoice/table-row-invoice.tsx rename to libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/table-row-invoice/table-row-invoice.tsx index 21bc5c07353..8f0128616f1 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/invoices-list/table-row-invoice/table-row-invoice.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-billing-summary/invoices-list-feature/table-row-invoice/table-row-invoice.tsx @@ -50,12 +50,12 @@ export function TableRowInvoice(props: TableRowInvoiceProps) { className="border-b bg-white last-of-type:border-b-0" > <> -
{dateMediumLocalFormat(data.created_at)}
-
{badge}
-
+
{dateMediumLocalFormat(data.created_at)}
+
{badge}
+
{costToHuman(data.total_in_cents / 100, data.currency_code)}
-
+
+ + )} +
+ Plan details +
+ + + +
+
+ +
+
+
Current plan
+
+ +
{formatPlanDisplay(props.currentCost?.plan)}
+
+
+ + See details + +
+
+
Current bill
+
+ +
+ + {costToHuman(props.currentCost?.cost?.total || 0, props.currentCost?.cost?.currency_code || 'USD')} + {' '} + / {billingRecurrence} +
+
+
+ {props.currentCost?.plan !== PlanEnum.FREE && ( +

+ Next invoice:{' '} + + {props.currentCost?.renewal_at && dateToFormat(props.currentCost?.renewal_at, 'MMM dd, Y')} + +

+ )} +
+ + {props.currentCost?.plan !== PlanEnum.FREE && ( +
+
Payment method
+
+ +
+ {props.creditCard ? ( + <> + + + **** {props.creditCard?.last_digit} + + + ) : ( + No credit card provided + )} +
+
+
+ + Edit payment + +
+ )} +
+ + +
+ ) +} + +export function SettingsBillingSummary() { + useDocumentTitle('Billing summary - Organization settings') + + const { openModal, closeModal } = useModal() + + const { organizationId = '' } = useParams({ strict: false }) + const navigate = useNavigate() + + const { data: creditCards = [], isLoading: isLoadingCreditCards } = useCreditCards({ organizationId }) + const { data: currentCost } = useCurrentCost({ organizationId }) + const { showChat } = useSupportChat() + const { isQoveryAdminUser } = useUserRole() + + const openPromoCodeModal = () => { + openModal({ + content: , + }) + } + + const openShowUsageModal = () => { + openModal({ + content: currentCost && , + }) + } + + const openPlanSelectionModal = () => { + openModal({ + content: ( + + ), + }) + } + + const handleChangePlanClick = () => { + if (isQoveryAdminUser) { + openPlanSelectionModal() + } else { + showChat() + } + } + + const handleCancelTrialClick = () => { + navigate(SETTINGS_URL(organizationId) + SETTINGS_DANGER_ZONE_URL) + } + + const handleAddCreditCardClick = () => { + openModal({ + content: , + }) + } + + return ( + 0} + onPromoCodeClick={openPromoCodeModal} + onShowUsageClick={openShowUsageModal} + onChangePlanClick={handleChangePlanClick} + onCancelTrialClick={handleCancelTrialClick} + onAddCreditCardClick={handleAddCreditCardClick} + /> + ) +} diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-summary-feature/show-usage-modal-feature/show-usage-modal-feature.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal-feature.spec.tsx similarity index 100% rename from libs/pages/settings/src/lib/feature/page-organization-billing-summary-feature/show-usage-modal-feature/show-usage-modal-feature.spec.tsx rename to libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal-feature.spec.tsx diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/show-usage-modal/show-usage-modal.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal-feature.tsx similarity index 70% rename from libs/pages/settings/src/lib/ui/page-organization-billing-summary/show-usage-modal/show-usage-modal.tsx rename to libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal-feature.tsx index 202cbf383a6..c58a7544790 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/show-usage-modal/show-usage-modal.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal-feature.tsx @@ -1,9 +1,14 @@ import { eachMonthOfInterval, isAfter, isBefore, isEqual } from 'date-fns' +import { type OrganizationCurrentCost } from 'qovery-typescript-axios' import { type Organization } from 'qovery-typescript-axios' +import { FormProvider, useForm } from 'react-hook-form' import { Controller, useFormContext } from 'react-hook-form' -import { useOrganization } from '@qovery/domains/organizations/feature' +import { useModal } from '@qovery/shared/ui' import { Callout, Icon, InputSelect, InputText, ModalCrud } from '@qovery/shared/ui' import { setDayOfTheMonth } from '@qovery/shared/util-dates' +import { useGenerateBillingUsageReport } from '../../hooks/use-generate-billing-usage-report/use-generate-billing-usage-report' +import { useOrganization } from '../../hooks/use-organization/use-organization' +import ShowUsageValueModal from './show-usage-value-modal/show-usage-value-modal' export interface ShowUsageModalProps { organizationId: string @@ -161,4 +166,60 @@ export function ShowUsageModal({ organizationId, renewalAt, onSubmit, onClose, l ) } -export default ShowUsageModal +export interface ShowUsageModalFeatureProps { + organizationId: string + currentCost: OrganizationCurrentCost +} + +export function ShowUsageModalFeature({ organizationId, currentCost }: ShowUsageModalFeatureProps) { + const methods = useForm<{ expires: number; report_period: string }>({ + defaultValues: { + expires: 24, + }, + mode: 'all', + }) + + const { data: organization } = useOrganization({ organizationId }) + const { mutateAsync: usageBillingReport, isLoading: isLoadingUsageBillingReport } = useGenerateBillingUsageReport() + const { openModal, closeModal } = useModal() + + const onSubmit = methods.handleSubmit(async (data) => { + if (!organization) return + + try { + const selectedReportPeriod = JSON.parse(data.report_period) + + const res = await usageBillingReport({ + organizationId, + usageReportRequest: { + from: new Date(new Date(selectedReportPeriod.from).getTime() + 24 * 60 * 60 * 1000).toISOString(), + to: selectedReportPeriod.to + ? new Date(new Date(selectedReportPeriod.to).getTime() + 24 * 60 * 60 * 1000).toISOString() + : new Date().toISOString(), + report_expiration_in_seconds: methods.getValues('expires') * 60 * 60, // hours to seconds + }, + }) + openModal({ + content: ( + + ), + }) + } catch (error) { + console.error(error) + } + }) + + return ( + + + + ) +} + +export default ShowUsageModalFeature diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/show-usage-modal/show-usage-modal.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal.spec.tsx similarity index 100% rename from libs/pages/settings/src/lib/ui/page-organization-billing-summary/show-usage-modal/show-usage-modal.spec.tsx rename to libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal.spec.tsx diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/show-usage-value-modal/show-usage-value-modal.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-value-modal/show-usage-value-modal.spec.tsx similarity index 100% rename from libs/pages/settings/src/lib/ui/page-organization-billing-summary/show-usage-value-modal/show-usage-value-modal.spec.tsx rename to libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-value-modal/show-usage-value-modal.spec.tsx diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/show-usage-value-modal/show-usage-value-modal.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-value-modal/show-usage-value-modal.tsx similarity index 92% rename from libs/pages/settings/src/lib/ui/page-organization-billing-summary/show-usage-value-modal/show-usage-value-modal.tsx rename to libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-value-modal/show-usage-value-modal.tsx index bdf5c51a580..fa061a7dfb6 100644 --- a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/show-usage-value-modal/show-usage-value-modal.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-value-modal/show-usage-value-modal.tsx @@ -9,7 +9,7 @@ export interface ShowUsageValueModalProps { export function ShowUsageValueModal(props: ShowUsageValueModalProps) { return (
-

+

Your report is ready{' '} 🎊 @@ -31,7 +31,7 @@ export function ShowUsageValueModal(props: ShowUsageValueModalProps) { value={props.url} disabled className="mb-1" - rightElement={} + rightElement={} />
-
- - + + + {dateMediumLocalFormat(data.created_at)} + + + {badge} + + + {costToHuman(data.total_in_cents / 100, data.currency_code)} + + + + + ) } diff --git a/libs/domains/organizations/feature/src/lib/settings-billing-summary/page-organization-billing-summary-feature.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/page-organization-billing-summary-feature.spec.tsx deleted file mode 100644 index 104cbd842fb..00000000000 --- a/libs/domains/organizations/feature/src/lib/settings-billing-summary/page-organization-billing-summary-feature.spec.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { IntercomProvider } from 'react-use-intercom' -import * as organizationsDomain from '@qovery/domains/organizations/feature' -import { renderWithProviders } from '@qovery/shared/util-tests' -import PageOrganizationBillingSummaryFeature from './page-organization-billing-summary-feature' - -import SpyInstance = jest.SpyInstance - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ organizationId: '1' }), -})) - -describe('PageOrganizationBillingSummaryFeature', () => { - it('should render successfully', () => { - const { baseElement } = renderWithProviders( - - - - ) - expect(baseElement).toBeTruthy() - }) - - it('should fetch credit card, clusters and organization costs', () => { - const useCreditCardsSpy: SpyInstance = jest.spyOn(organizationsDomain, 'useCreditCards') - const useCurrentCostSpy: SpyInstance = jest.spyOn(organizationsDomain, 'useCurrentCost') - - renderWithProviders( - - - - ) - - expect(useCreditCardsSpy).toHaveBeenCalled() - expect(useCurrentCostSpy).toHaveBeenCalled() - }) -}) diff --git a/libs/domains/organizations/feature/src/lib/settings-billing-summary/page-organization-billing-summary.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/page-organization-billing-summary.spec.tsx deleted file mode 100644 index 7bba7be072a..00000000000 --- a/libs/domains/organizations/feature/src/lib/settings-billing-summary/page-organization-billing-summary.spec.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { PlanEnum } from 'qovery-typescript-axios' -import { creditCardsFactoryMock } from '@qovery/shared/factories' -import { renderWithProviders, screen } from '@qovery/shared/util-tests' -import PageOrganizationBillingSummary, { - type PageOrganizationBillingSummaryProps, -} from '../../../../../../pages/settings/src/lib/ui/page-organization-billing-summary/page-organization-billing-summary' - -const props: PageOrganizationBillingSummaryProps = { - creditCard: creditCardsFactoryMock(1)[0], - creditCardLoading: false, - currentCost: { - plan: PlanEnum.ENTERPRISE, - cost: { total: 56000, currency_code: 'USD', total_in_cents: 5600000 }, - renewal_at: '2021-01-01', - }, - onPromoCodeClick: jest.fn(), - onShowUsageClick: jest.fn(), - onChangePlanClick: jest.fn(), -} - -describe('PageOrganizationBillingSummary', () => { - it('should render successfully', () => { - const { baseElement } = renderWithProviders() - expect(baseElement).toBeTruthy() - }) - - it('should display 6 boxes', () => { - renderWithProviders() - screen.getByText('Current plan') - screen.getByText('Enterprise plan (Legacy)') - - screen.getByText('Current bill') - screen.getByText('$56,000') - }) - - it('should say that no credit card was found', () => { - renderWithProviders() - screen.getByText('No credit card provided') - }) - - it('should say call show usage modal on click', async () => { - const { userEvent } = renderWithProviders() - const button = screen.getByText(/show usage/i) - await userEvent.click(button) - - expect(props.onShowUsageClick).toHaveBeenCalled() - }) - - it('should call onChangePlanClick on click on change plan', async () => { - const { userEvent } = renderWithProviders() - const button = screen.getByText('Change plan') - await userEvent.click(button) - - expect(props.onChangePlanClick).toHaveBeenCalled() - }) - - it('should say call onPromoCodeClick on click on add promo code', async () => { - const { userEvent } = renderWithProviders() - const button = screen.getByText(/promo code/i) - await userEvent.click(button) - - expect(props.onPromoCodeClick).toHaveBeenCalled() - }) - - it('should display not display the payment method box', () => { - props.currentCost = { - plan: PlanEnum.FREE, - renewal_at: '2021-01-01', - cost: { total: 56000, currency_code: 'USD', total_in_cents: 5600000 }, - } - - renderWithProviders() - screen.getByText('Current plan') - screen.getByText('Current bill') - expect(screen.queryByText('Payment method')).not.toBeInTheDocument() - }) -}) diff --git a/libs/domains/organizations/feature/src/lib/settings-billing-summary/promo-code-modal-feature/promo-code-modal-feature.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/promo-code-modal-feature/promo-code-modal-feature.spec.tsx index d7ec591e353..284e8aced8f 100644 --- a/libs/domains/organizations/feature/src/lib/settings-billing-summary/promo-code-modal-feature/promo-code-modal-feature.spec.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-billing-summary/promo-code-modal-feature/promo-code-modal-feature.spec.tsx @@ -1,28 +1,68 @@ -import * as organizationsDomain from '@qovery/domains/organizations/feature' +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' import { renderWithProviders, screen } from '@qovery/shared/util-tests' -import PromoCodeModalFeature, { type PromocodeModalFeatureProps } from './promo-code-modal-feature' +import * as addCreditCodeHooks from '../../hooks/use-add-credit-code/use-add-credit-code' +import PromoCodeModalFeature, { + PromoCodeModal, + type PromoCodeModalProps, + type PromocodeModalFeatureProps, +} from './promo-code-modal-feature' -const useAddCreditCodeSpy = jest.spyOn(organizationsDomain, 'useAddCreditCode') as jest.Mock +const useAddCreditCodeSpy = jest.spyOn(addCreditCodeHooks, 'useAddCreditCode') -const props: PromocodeModalFeatureProps = { +const featureProps: PromocodeModalFeatureProps = { closeModal: jest.fn(), organizationId: '1', } +const modalProps: PromoCodeModalProps = { + isSubmitting: false, + onClose: jest.fn(), + onSubmit: jest.fn((event) => event.preventDefault()), +} + +describe('PromoCodeModal', () => { + it('should render successfully', () => { + const { baseElement } = renderWithProviders(wrapWithReactHookForm()) + expect(baseElement).toBeTruthy() + }) + + it('should show spinner when submitting', () => { + renderWithProviders(wrapWithReactHookForm()) + screen.getByTestId('spinner') + }) + + it('should call onSubmit when clicking submit', async () => { + const onSubmit = jest.fn((event) => event.preventDefault()) + + const { userEvent } = renderWithProviders( + wrapWithReactHookForm(, { + defaultValues: { code: 'test' }, + }) + ) + + await userEvent.click(screen.getByTestId('submit-button')) + + expect(onSubmit).toHaveBeenCalled() + }) +}) + describe('PromoCodeModalFeature', () => { + const mutateAsyncMock = jest.fn() + beforeEach(() => { + mutateAsyncMock.mockReset() useAddCreditCodeSpy.mockReturnValue({ - mutateAsync: jest.fn(), + mutateAsync: mutateAsyncMock, }) }) it('should render successfully', () => { - const { baseElement } = renderWithProviders() + const { baseElement } = renderWithProviders() expect(baseElement).toBeTruthy() }) it('should useAddCreditCode with good params', async () => { - const { userEvent } = renderWithProviders() + const { userEvent } = renderWithProviders() const input = screen.getByLabelText('Promo code') await userEvent.type(input, 'test') @@ -30,6 +70,6 @@ describe('PromoCodeModalFeature', () => { const button = screen.getByTestId('submit-button') await userEvent.click(button) - expect(useAddCreditCodeSpy().mutateAsync).toHaveBeenCalledWith({ organizationId: '1', code: 'test' }) + expect(mutateAsyncMock).toHaveBeenCalledWith({ organizationId: '1', code: 'test' }) }) }) diff --git a/libs/domains/organizations/feature/src/lib/settings-billing-summary/promo-code-modal-feature/promo-code-modal.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/promo-code-modal-feature/promo-code-modal.spec.tsx deleted file mode 100644 index b2656755db5..00000000000 --- a/libs/domains/organizations/feature/src/lib/settings-billing-summary/promo-code-modal-feature/promo-code-modal.spec.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { act, getByTestId, render } from '__tests__/utils/setup-jest' -import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' -import PromoCodeModal, { type PromoCodeModalProps } from './promo-code-modal' - -const props: PromoCodeModalProps = { - isSubmitting: false, - onClose: jest.fn(), - onSubmit: jest.fn((e) => e.preventDefault()), -} - -describe('PromocodeModal', () => { - it('should render successfully', () => { - const { baseElement } = render(wrapWithReactHookForm<{ code: string }>()) - expect(baseElement).toBeTruthy() - }) - - it('should show spinner', () => { - const { baseElement } = render( - wrapWithReactHookForm<{ code: string }>() - ) - getByTestId(baseElement, 'spinner') - }) - - it('should call on Submit', async () => { - const spy = jest.fn((e) => e.preventDefault()) - props.onSubmit = spy - - const { baseElement } = render( - wrapWithReactHookForm<{ code: string }>(, { - defaultValues: { code: 'test' }, - }) - ) - const button = getByTestId(baseElement, 'submit-button') - await act(() => { - button.click() - }) - - expect(spy).toHaveBeenCalled() - }) -}) diff --git a/libs/domains/organizations/feature/src/lib/settings-billing-summary/settings-billing-summary.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/settings-billing-summary.spec.tsx new file mode 100644 index 00000000000..6599d327962 --- /dev/null +++ b/libs/domains/organizations/feature/src/lib/settings-billing-summary/settings-billing-summary.spec.tsx @@ -0,0 +1,156 @@ +import { PlanEnum } from 'qovery-typescript-axios' +import { type ReactNode } from 'react' +import { useUserSignUp } from '@qovery/domains/users-sign-up/feature' +import { creditCardsFactoryMock } from '@qovery/shared/factories' +import { useUserRole } from '@qovery/shared/iam/feature' +import * as sharedUi from '@qovery/shared/ui' +import { useDocumentTitle, useSupportChat } from '@qovery/shared/util-hooks' +import { costToHuman, formatPlanDisplay } from '@qovery/shared/util-js' +import { renderWithProviders, screen } from '@qovery/shared/util-tests' +import { useCreditCards } from '../hooks/use-credit-cards/use-credit-cards' +import { useCurrentCost } from '../hooks/use-current-cost/use-current-cost' +import { + PageOrganizationBillingSummary, + type PageOrganizationBillingSummaryProps, + SettingsBillingSummary, +} from './settings-billing-summary' + +jest.mock('../hooks/use-credit-cards/use-credit-cards') +jest.mock('../hooks/use-current-cost/use-current-cost') +jest.mock('@qovery/domains/users-sign-up/feature') +jest.mock('@qovery/shared/iam/feature') +jest.mock('@qovery/shared/util-hooks') +jest.mock('./invoices-list-feature/invoices-list-feature', () => ({ + __esModule: true, + default: () => null, +})) + +jest.mock('@tanstack/react-router', () => ({ + ...jest.requireActual('@tanstack/react-router'), + useParams: () => ({ organizationId: '1' }), + useNavigate: () => jest.fn(), + Link: ({ children, ...props }: { children: ReactNode }) => {children}, +})) + +const creditCardsMock = creditCardsFactoryMock(1) + +const currentCostMock = { + plan: PlanEnum.ENTERPRISE, + cost: { total: 56000, currency_code: 'USD', total_in_cents: 5600000 }, + renewal_at: '2021-01-01', + remaining_trial_day: 0, +} + +const pageProps: PageOrganizationBillingSummaryProps = { + creditCard: creditCardsMock[0], + currentCost: currentCostMock, + hasCreditCard: true, + onPromoCodeClick: jest.fn(), + onShowUsageClick: jest.fn(), + onChangePlanClick: jest.fn(), +} + +describe('PageOrganizationBillingSummary', () => { + const useUserSignUpMock = useUserSignUp as jest.MockedFunction + + beforeEach(() => { + jest.clearAllMocks() + useUserSignUpMock.mockReturnValue({ + data: { dx_auth: false }, + } as unknown as ReturnType) + }) + + it('should render successfully', () => { + const { baseElement } = renderWithProviders() + expect(baseElement).toBeTruthy() + }) + + it('should display plan and bill details', () => { + renderWithProviders() + screen.getByText('Current plan') + screen.getByText(formatPlanDisplay(currentCostMock.plan)) + screen.getByText('Current bill') + screen.getByText(costToHuman(currentCostMock.cost.total, currentCostMock.cost.currency_code)) + }) + + it('should say that no credit card was found', () => { + renderWithProviders() + screen.getByText('No credit card provided') + }) + + it('should call onShowUsageClick on click', async () => { + const { userEvent } = renderWithProviders() + await userEvent.click(screen.getByText(/show usage/i)) + expect(pageProps.onShowUsageClick).toHaveBeenCalled() + }) + + it('should call onChangePlanClick on click', async () => { + const { userEvent } = renderWithProviders() + await userEvent.click(screen.getByText(/change plan/i)) + expect(pageProps.onChangePlanClick).toHaveBeenCalled() + }) + + it('should call onPromoCodeClick on click', async () => { + const { userEvent } = renderWithProviders() + await userEvent.click(screen.getByText(/promo code/i)) + expect(pageProps.onPromoCodeClick).toHaveBeenCalled() + }) + + it('should not display the payment method box for free plan', () => { + renderWithProviders( + + ) + expect(screen.queryByText('Payment method')).not.toBeInTheDocument() + }) +}) + +describe('SettingsBillingSummary', () => { + const useCreditCardsMock = useCreditCards as jest.MockedFunction + const useCurrentCostMock = useCurrentCost as jest.MockedFunction + const useUserSignUpMock = useUserSignUp as jest.MockedFunction + const useUserRoleMock = useUserRole as jest.MockedFunction + const useSupportChatMock = useSupportChat as jest.MockedFunction + const useDocumentTitleMock = useDocumentTitle as jest.MockedFunction + const useModalSpy = jest.spyOn(sharedUi, 'useModal') + + beforeEach(() => { + useCreditCardsMock.mockReturnValue({ + data: creditCardsMock, + } as unknown as ReturnType) + useCurrentCostMock.mockReturnValue({ + data: currentCostMock, + } as unknown as ReturnType) + useUserSignUpMock.mockReturnValue({ + data: { dx_auth: false }, + } as unknown as ReturnType) + useUserRoleMock.mockReturnValue({ + isQoveryAdminUser: true, + } as unknown as ReturnType) + useSupportChatMock.mockReturnValue({ + showChat: jest.fn(), + } as unknown as ReturnType) + useDocumentTitleMock.mockImplementation(() => undefined) + useModalSpy.mockReturnValue({ + openModal: jest.fn(), + closeModal: jest.fn(), + } as unknown as ReturnType) + }) + + it('should render successfully', () => { + const { baseElement } = renderWithProviders() + expect(baseElement).toBeTruthy() + }) + + it('should fetch credit cards and current cost', () => { + renderWithProviders() + + expect(useCreditCardsMock).toHaveBeenCalledWith({ organizationId: '1', suspense: true }) + expect(useCurrentCostMock).toHaveBeenCalledWith({ organizationId: '1', suspense: true }) + }) +}) diff --git a/libs/domains/organizations/feature/src/lib/settings-billing-summary/settings-billing-summary.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/settings-billing-summary.tsx index c201a904be8..8211ffedbed 100644 --- a/libs/domains/organizations/feature/src/lib/settings-billing-summary/settings-billing-summary.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-billing-summary/settings-billing-summary.tsx @@ -1,24 +1,13 @@ import { useNavigate, useParams } from '@tanstack/react-router' import { format } from 'date-fns' import { type CreditCard, type OrganizationCurrentCost, PlanEnum } from 'qovery-typescript-axios' -import { useMemo } from 'react' +import { Suspense, useMemo } from 'react' import { type CardImages } from 'react-payment-inputs/images' import { useUserSignUp } from '@qovery/domains/users-sign-up/feature' -import { AddCreditCardModalFeature } from '@qovery/shared/console-shared' +import { AddCreditCardModalFeature, SettingsHeading } from '@qovery/shared/console-shared' import { useUserRole } from '@qovery/shared/iam/feature' -import { SETTINGS_BILLING_URL, SETTINGS_DANGER_ZONE_URL, SETTINGS_URL } from '@qovery/shared/routes' import { useModal } from '@qovery/shared/ui' -import { - Button, - Callout, - ExternalLink, - Heading, - Icon, - Link, - Section, - Skeleton, - imagesCreditCart, -} from '@qovery/shared/ui' +import { Button, Callout, ExternalLink, Icon, Link, Section, Skeleton, imagesCreditCart } from '@qovery/shared/ui' import { dateToFormat } from '@qovery/shared/util-dates' import { useDocumentTitle, useSupportChat } from '@qovery/shared/util-hooks' import { costToHuman, formatPlanDisplay, pluralize } from '@qovery/shared/util-js' @@ -29,10 +18,9 @@ import PlanSelectionModalFeature from './plan-selection-modal-feature/plan-selec import PromoCodeModalFeature from './promo-code-modal-feature/promo-code-modal-feature' import ShowUsageModalFeature from './show-usage-modal-feature/show-usage-modal-feature' -interface PageOrganizationBillingSummaryProps { +export interface PageOrganizationBillingSummaryProps { currentCost?: OrganizationCurrentCost creditCard?: CreditCard - creditCardLoading?: boolean hasCreditCard?: boolean onPromoCodeClick?: () => void onShowUsageClick?: () => void @@ -55,7 +43,53 @@ function getBillingRecurrenceStr(renewalAt: string | null | undefined): string { return 'month' } -function PageOrganizationBillingSummary(props: PageOrganizationBillingSummaryProps) { +const BillingSummarySkeleton = () => ( +
+
+
+ +
+ + + +
+
+
+
+ {Array.from({ length: 3 }).map((_, index) => ( +
+ +
+ +
+
+ +
+
+ ))} +
+
+
+ + +
+
+ {Array.from({ length: 4 }).map((_, index) => ( +
+ + + + +
+ ))} +
+
+
+
+
+) + +export function PageOrganizationBillingSummary(props: PageOrganizationBillingSummaryProps) { const { organizationId = '' } = useParams({ strict: false }) const { data: userSignUp } = useUserSignUp() @@ -63,7 +97,7 @@ function PageOrganizationBillingSummary(props: PageOrganizationBillingSummaryPro // It's not so accurate, but it's a good enough approximation for now const billingRecurrence = getBillingRecurrenceStr(props.currentCost?.renewal_at) const remainingTrialDay = props.currentCost?.remaining_trial_day ?? 0 - const showTrialCallout = remainingTrialDay !== undefined && remainingTrialDay > 0 && !props.creditCardLoading + const showTrialCallout = remainingTrialDay !== undefined && remainingTrialDay > 0 const showErrorCallout = (props.hasCreditCard ?? Boolean(props.creditCard)) || userSignUp?.dx_auth // This function is used to get the trial start date based on the remaining trial days from the API @@ -87,40 +121,11 @@ function PageOrganizationBillingSummary(props: PageOrganizationBillingSummaryPro }, [props.currentCost?.remaining_trial_day]) return ( -
+
- {showTrialCallout && ( - - - - {/* Add + 1 because Chargebee return 0 when the trial is ending today */} - {showErrorCallout - ? `Your free trial plan expires ${remainingTrialDay + 1} ${pluralize(remainingTrialDay + 1, 'day')} from now` - : `No credit card registered, your account will be blocked at the end your trial in ${remainingTrialDay + 1} ${pluralize(remainingTrialDay + 1, 'day')}`} - - {showErrorCallout ? ( - <> - You have contracted a free 14-days trial on{' '} - {trialStartDate ? format(trialStartDate, 'MMMM d, yyyy') : '...'}. At the end of this plan your user - subscription will start. You cancel your trial by deleting your organization. - - ) : ( - <>Add a payment method to avoid service interruption at the end of your trial. - )} - - - - )} -
- Plan details -
+
+ +
- -
-
-
Current plan
-
- +
+ {showTrialCallout && ( + + + + {/* Add + 1 because Chargebee return 0 when the trial is ending today */} + {showErrorCallout + ? `Your free trial plan expires ${remainingTrialDay + 1} ${pluralize(remainingTrialDay + 1, 'day')} from now` + : `No credit card registered, your account will be blocked at the end your trial in ${remainingTrialDay + 1} ${pluralize(remainingTrialDay + 1, 'day')}`} + + {showErrorCallout ? ( + <> + You have contracted a free 14-days trial on{' '} + {trialStartDate ? format(trialStartDate, 'MMMM d, yyyy') : '...'}. At the end of this plan your user + subscription will start. You cancel your trial by deleting your organization. + + ) : ( + <>Add a payment method to avoid service interruption at the end of your trial. + )} + + + + )} +
+
+
Current plan
+
{formatPlanDisplay(props.currentCost?.plan)}
- +
+ + See details +
- - See details - -
-
-
Current bill
-
- +
+
Current bill
+
- + {costToHuman(props.currentCost?.cost?.total || 0, props.currentCost?.cost?.currency_code || 'USD')} {' '} - / {billingRecurrence} + / {billingRecurrence}
- +
+ {props.currentCost?.plan !== PlanEnum.FREE && ( +

+ Next invoice:{' '} + + {props.currentCost?.renewal_at && dateToFormat(props.currentCost?.renewal_at, 'MMM dd, Y')} + +

+ )}
- {props.currentCost?.plan !== PlanEnum.FREE && ( -

- Next invoice:{' '} - - {props.currentCost?.renewal_at && dateToFormat(props.currentCost?.renewal_at, 'MMM dd, Y')} - -

- )} -
- {props.currentCost?.plan !== PlanEnum.FREE && ( -
-
Payment method
-
- + {props.currentCost?.plan !== PlanEnum.FREE && ( +
+
Payment method
+
{props.creditCard ? ( <> - + **** {props.creditCard?.last_digit} ) : ( - No credit card provided + No credit card provided )}
- +
+ + Edit payment +
- - Edit payment - -
- )} + )} +
+
-
) } -export function SettingsBillingSummary() { - useDocumentTitle('Billing summary - Organization settings') - +function SettingsBillingSummaryContent() { const { openModal, closeModal } = useModal() const { organizationId = '' } = useParams({ strict: false }) const navigate = useNavigate() - const { data: creditCards = [], isLoading: isLoadingCreditCards } = useCreditCards({ organizationId }) - const { data: currentCost } = useCurrentCost({ organizationId }) + const { data: creditCards = [] } = useCreditCards({ organizationId, suspense: true }) + const { data: currentCost } = useCurrentCost({ organizationId, suspense: true }) const { showChat } = useSupportChat() const { isQoveryAdminUser } = useUserRole() @@ -245,7 +272,7 @@ export function SettingsBillingSummary() { } const handleCancelTrialClick = () => { - navigate(SETTINGS_URL(organizationId) + SETTINGS_DANGER_ZONE_URL) + navigate({ to: '/organization/$organizationId/settings/danger-zone', params: { organizationId } }) } const handleAddCreditCardClick = () => { @@ -258,7 +285,6 @@ export function SettingsBillingSummary() { 0} onPromoCodeClick={openPromoCodeModal} onShowUsageClick={openShowUsageModal} @@ -268,3 +294,13 @@ export function SettingsBillingSummary() { /> ) } + +export function SettingsBillingSummary() { + useDocumentTitle('Billing summary - Organization settings') + + return ( + }> + + + ) +} diff --git a/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal-feature.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal-feature.spec.tsx index 1b7b4ece63b..d5b19163a32 100644 --- a/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal-feature.spec.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal-feature.spec.tsx @@ -1,11 +1,23 @@ +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' import { PlanEnum } from 'qovery-typescript-axios' -import * as organizationsDomain from '@qovery/domains/organizations/feature' import { organizationFactoryMock } from '@qovery/shared/factories' -import { renderWithProviders, screen } from '@qovery/shared/util-tests' -import ShowUsageModalFeature, { type ShowUsageModalFeatureProps } from './show-usage-modal-feature' +import { renderWithProviders, screen, waitFor } from '@qovery/shared/util-tests' +import { useGenerateBillingUsageReport } from '../../hooks/use-generate-billing-usage-report/use-generate-billing-usage-report' +import { useOrganization } from '../../hooks/use-organization/use-organization' +import ShowUsageModalFeature, { + ShowUsageModal, + type ShowUsageModalFeatureProps, + type ShowUsageModalProps, + getReportPeriods, +} from './show-usage-modal-feature' -const useGenerateBillingUsageReportSpy = jest.spyOn(organizationsDomain, 'useGenerateBillingUsageReport') as jest.Mock -const useOrganizationSpy = jest.spyOn(organizationsDomain, 'useOrganization') as jest.Mock +jest.mock('../../hooks/use-generate-billing-usage-report/use-generate-billing-usage-report') +jest.mock('../../hooks/use-organization/use-organization') + +const mockOrganization = { + ...organizationFactoryMock(1)[0], + created_at: '2023-06-16T14:34:04Z', +} const props: ShowUsageModalFeatureProps = { organizationId: '1', @@ -21,17 +33,67 @@ const props: ShowUsageModalFeatureProps = { }, } +describe('ShowUsageModal', () => { + const useOrganizationMock = useOrganization as jest.MockedFunction + + const modalProps: ShowUsageModalProps = { + organizationId: '0', + renewalAt: '2022-01-01T00:00:00Z', + onClose: jest.fn(), + onSubmit: jest.fn((event) => event.preventDefault()), + loading: false, + } + + beforeEach(() => { + useOrganizationMock.mockReturnValue({ + data: mockOrganization, + } as unknown as ReturnType) + }) + + it('should render successfully', () => { + const { baseElement } = renderWithProviders(wrapWithReactHookForm()) + expect(baseElement).toBeTruthy() + }) + + it('should call onSubmit', async () => { + const onSubmit = jest.fn((event) => event.preventDefault()) + const reportPeriods = getReportPeriods({ organization: mockOrganization, orgRenewalAt: modalProps.renewalAt }) + + const { userEvent } = renderWithProviders( + wrapWithReactHookForm(, { + defaultValues: { + expires: 24, + report_period: reportPeriods[0]?.value, + }, + }) + ) + + await userEvent.click(screen.getByTestId('submit-button')) + + expect(onSubmit).toHaveBeenCalled() + }) +}) + describe('ShowUsageModalFeature', () => { + const useGenerateBillingUsageReportMock = useGenerateBillingUsageReport as jest.MockedFunction< + typeof useGenerateBillingUsageReport + > + const useOrganizationMock = useOrganization as jest.MockedFunction + + const mutateAsyncMock = jest.fn() + beforeEach(() => { - useGenerateBillingUsageReportSpy.mockReturnValue({ - mutateAsync: jest.fn(() => ({ - report_url: 'http://example.com', - })), - isLoading: false, - }) - useOrganizationSpy.mockReturnValue({ - data: organizationFactoryMock(1)[0], + mutateAsyncMock.mockReset() + mutateAsyncMock.mockResolvedValue({ + report_url: 'http://example.com', }) + useGenerateBillingUsageReportMock.mockReturnValue({ + mutateAsync: mutateAsyncMock, + isLoading: false, + } as unknown as ReturnType) + useOrganizationMock.mockReturnValue({ + data: mockOrganization, + } as unknown as ReturnType) }) it('should render successfully', () => { @@ -41,9 +103,209 @@ describe('ShowUsageModalFeature', () => { it('should call useGenerateBillingUsageReport when onSubmit is called', async () => { const { userEvent } = renderWithProviders() - const button = screen.getByRole('button', { name: /generate report/i }) - await userEvent.click(button) - expect(useGenerateBillingUsageReportSpy).toHaveBeenCalled() + await userEvent.click(screen.getByRole('button', { name: /generate report/i })) + + await waitFor(() => { + expect(mutateAsyncMock).toHaveBeenCalledWith( + expect.objectContaining({ + organizationId: '1', + usageReportRequest: expect.objectContaining({ + report_expiration_in_seconds: 24 * 60 * 60, + }), + }) + ) + }) + }) +}) + +describe('getReportPeriods', () => { + beforeEach(() => { + const now = new Date('2024-04-23T12:00:00Z') + jest.useFakeTimers() + jest.setSystemTime(now) + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('should compute periods given no renewal date', () => { + const organization = organizationFactoryMock(1)[0] + organization.created_at = '2023-06-16T14:34:04Z' + expect(getReportPeriods({ organization })).toEqual([ + { + label: 'Apr 16, 2024 to now', + value: '{"from":"2024-04-16T00:00:00.000Z"}', + }, + { + label: 'Mar 16, 2024 to Apr 16, 2024', + value: '{"from":"2024-03-16T00:00:00.000Z","to":"2024-04-16T00:00:00.000Z"}', + }, + { + label: 'Feb 16, 2024 to Mar 16, 2024', + value: '{"from":"2024-02-16T00:00:00.000Z","to":"2024-03-16T00:00:00.000Z"}', + }, + { + label: 'Jan 16, 2024 to Feb 16, 2024', + value: '{"from":"2024-01-16T00:00:00.000Z","to":"2024-02-16T00:00:00.000Z"}', + }, + { + label: 'Dec 16, 2023 to Jan 16, 2024', + value: '{"from":"2023-12-16T00:00:00.000Z","to":"2024-01-16T00:00:00.000Z"}', + }, + { + label: 'Nov 16, 2023 to Dec 16, 2023', + value: '{"from":"2023-11-16T00:00:00.000Z","to":"2023-12-16T00:00:00.000Z"}', + }, + { + label: 'Oct 16, 2023 to Nov 16, 2023', + value: '{"from":"2023-10-16T00:00:00.000Z","to":"2023-11-16T00:00:00.000Z"}', + }, + { + label: 'Sep 16, 2023 to Oct 16, 2023', + value: '{"from":"2023-09-16T00:00:00.000Z","to":"2023-10-16T00:00:00.000Z"}', + }, + { + label: 'Aug 16, 2023 to Sep 16, 2023', + value: '{"from":"2023-08-16T00:00:00.000Z","to":"2023-09-16T00:00:00.000Z"}', + }, + { + label: 'Jul 16, 2023 to Aug 16, 2023', + value: '{"from":"2023-07-16T00:00:00.000Z","to":"2023-08-16T00:00:00.000Z"}', + }, + { + label: 'Jun 16, 2023 to Jul 16, 2023', + value: '{"from":"2023-06-16T00:00:00.000Z","to":"2023-07-16T00:00:00.000Z"}', + }, + ]) + }) + + it('should compute periods given a renewal date with a day in month before creation date', () => { + const organization = organizationFactoryMock(1)[0] + organization.created_at = '2023-06-16T14:34:04Z' + const orgRenewalAt = '2023-07-10T14:34:04Z' + expect(getReportPeriods({ organization, orgRenewalAt })).toEqual([ + { + label: 'Apr 10, 2024 to now', + value: '{"from":"2024-04-10T00:00:00.000Z"}', + }, + { + label: 'Mar 10, 2024 to Apr 10, 2024', + value: '{"from":"2024-03-10T00:00:00.000Z","to":"2024-04-10T00:00:00.000Z"}', + }, + { + label: 'Feb 10, 2024 to Mar 10, 2024', + value: '{"from":"2024-02-10T00:00:00.000Z","to":"2024-03-10T00:00:00.000Z"}', + }, + { + label: 'Jan 10, 2024 to Feb 10, 2024', + value: '{"from":"2024-01-10T00:00:00.000Z","to":"2024-02-10T00:00:00.000Z"}', + }, + { + label: 'Dec 10, 2023 to Jan 10, 2024', + value: '{"from":"2023-12-10T00:00:00.000Z","to":"2024-01-10T00:00:00.000Z"}', + }, + { + label: 'Nov 10, 2023 to Dec 10, 2023', + value: '{"from":"2023-11-10T00:00:00.000Z","to":"2023-12-10T00:00:00.000Z"}', + }, + { + label: 'Oct 10, 2023 to Nov 10, 2023', + value: '{"from":"2023-10-10T00:00:00.000Z","to":"2023-11-10T00:00:00.000Z"}', + }, + { + label: 'Sep 10, 2023 to Oct 10, 2023', + value: '{"from":"2023-09-10T00:00:00.000Z","to":"2023-10-10T00:00:00.000Z"}', + }, + { + label: 'Aug 10, 2023 to Sep 10, 2023', + value: '{"from":"2023-08-10T00:00:00.000Z","to":"2023-09-10T00:00:00.000Z"}', + }, + { + label: 'Jul 10, 2023 to Aug 10, 2023', + value: '{"from":"2023-07-10T00:00:00.000Z","to":"2023-08-10T00:00:00.000Z"}', + }, + ]) + }) + + it('should compute periods given a renewal date with a day in month after creation date', () => { + const organization = organizationFactoryMock(1)[0] + organization.created_at = '2023-06-16T14:34:04Z' + const orgRenewalAt = '2023-07-21T14:34:04Z' + expect(getReportPeriods({ organization, orgRenewalAt })).toEqual([ + { + label: 'Apr 21, 2024 to now', + value: '{"from":"2024-04-21T00:00:00.000Z"}', + }, + { + label: 'Mar 21, 2024 to Apr 21, 2024', + value: '{"from":"2024-03-21T00:00:00.000Z","to":"2024-04-21T00:00:00.000Z"}', + }, + { + label: 'Feb 21, 2024 to Mar 21, 2024', + value: '{"from":"2024-02-21T00:00:00.000Z","to":"2024-03-21T00:00:00.000Z"}', + }, + { + label: 'Jan 21, 2024 to Feb 21, 2024', + value: '{"from":"2024-01-21T00:00:00.000Z","to":"2024-02-21T00:00:00.000Z"}', + }, + { + label: 'Dec 21, 2023 to Jan 21, 2024', + value: '{"from":"2023-12-21T00:00:00.000Z","to":"2024-01-21T00:00:00.000Z"}', + }, + { + label: 'Nov 21, 2023 to Dec 21, 2023', + value: '{"from":"2023-11-21T00:00:00.000Z","to":"2023-12-21T00:00:00.000Z"}', + }, + { + label: 'Oct 21, 2023 to Nov 21, 2023', + value: '{"from":"2023-10-21T00:00:00.000Z","to":"2023-11-21T00:00:00.000Z"}', + }, + { + label: 'Sep 21, 2023 to Oct 21, 2023', + value: '{"from":"2023-09-21T00:00:00.000Z","to":"2023-10-21T00:00:00.000Z"}', + }, + { + label: 'Aug 21, 2023 to Sep 21, 2023', + value: '{"from":"2023-08-21T00:00:00.000Z","to":"2023-09-21T00:00:00.000Z"}', + }, + { + label: 'Jul 21, 2023 to Aug 21, 2023', + value: '{"from":"2023-07-21T00:00:00.000Z","to":"2023-08-21T00:00:00.000Z"}', + }, + { + label: 'Jun 21, 2023 to Jul 21, 2023', + value: '{"from":"2023-06-21T00:00:00.000Z","to":"2023-07-21T00:00:00.000Z"}', + }, + ]) + }) + + it('should compute periods given a renewal date next year', () => { + const organization = organizationFactoryMock(1)[0] + organization.created_at = '2023-12-16T14:34:04Z' + const orgRenewalAt = '2024-01-21T14:34:04Z' + expect(getReportPeriods({ organization, orgRenewalAt })).toEqual([ + { + label: 'Apr 21, 2024 to now', + value: '{"from":"2024-04-21T00:00:00.000Z"}', + }, + { + label: 'Mar 21, 2024 to Apr 21, 2024', + value: '{"from":"2024-03-21T00:00:00.000Z","to":"2024-04-21T00:00:00.000Z"}', + }, + { + label: 'Feb 21, 2024 to Mar 21, 2024', + value: '{"from":"2024-02-21T00:00:00.000Z","to":"2024-03-21T00:00:00.000Z"}', + }, + { + label: 'Jan 21, 2024 to Feb 21, 2024', + value: '{"from":"2024-01-21T00:00:00.000Z","to":"2024-02-21T00:00:00.000Z"}', + }, + { + label: 'Dec 21, 2023 to Jan 21, 2024', + value: '{"from":"2023-12-21T00:00:00.000Z","to":"2024-01-21T00:00:00.000Z"}', + }, + ]) }) }) diff --git a/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal.spec.tsx deleted file mode 100644 index 59453645e74..00000000000 --- a/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-modal.spec.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' -import * as organizationsDomain from '@qovery/domains/organizations/feature' -import { organizationFactoryMock } from '@qovery/shared/factories' -import { renderWithProviders, screen } from '@qovery/shared/util-tests' -import ShowUsageModal, { type ShowUsageModalProps, getReportPeriods } from './show-usage-modal' - -const useOrganizationsSpy: SpyInstance = jest.spyOn(organizationsDomain, 'useOrganizations') - -const props: ShowUsageModalProps = { - organizationId: '0', - renewalAt: '2022-01-01T00:00:00Z', - onClose: jest.fn(), - onSubmit: jest.fn(), - loading: true, -} - -describe('ShowUsageModal', () => { - beforeEach(() => { - useOrganizationsSpy.mockReturnValue({ - data: organizationFactoryMock(1)[0], - }) - }) - - it('should render successfully', () => { - const { baseElement } = renderWithProviders(wrapWithReactHookForm<{ code: string }>()) - expect(baseElement).toBeTruthy() - }) - - it('should call on submit', async () => { - const spy = jest.fn() - props.loading = false - props.onSubmit = spy - - const { userEvent } = renderWithProviders( - wrapWithReactHookForm(, { - defaultValues: { - expires: 24, - }, - }) - ) - - const button = screen.getByTestId('submit-button') - await userEvent.click(button) - - expect(spy).toHaveBeenCalled() - }) -}) - -describe('getReportPeriods', () => { - beforeEach(() => { - const now = new Date('2024-04-23T12:00:00Z') - jest.useFakeTimers() - jest.setSystemTime(now) - }) - - afterEach(() => { - jest.useRealTimers() - }) - - it('should compute periods given no renewal date', () => { - const organization = organizationFactoryMock(1)[0] - organization.created_at = '2023-06-16T14:34:04Z' - expect(getReportPeriods({ organization })).toEqual([ - { - label: 'Apr 16, 2024 to now', - value: '{"from":"2024-04-16T00:00:00.000Z"}', - }, - { - label: 'Mar 16, 2024 to Apr 16, 2024', - value: '{"from":"2024-03-16T00:00:00.000Z","to":"2024-04-16T00:00:00.000Z"}', - }, - { - label: 'Feb 16, 2024 to Mar 16, 2024', - value: '{"from":"2024-02-16T00:00:00.000Z","to":"2024-03-16T00:00:00.000Z"}', - }, - { - label: 'Jan 16, 2024 to Feb 16, 2024', - value: '{"from":"2024-01-16T00:00:00.000Z","to":"2024-02-16T00:00:00.000Z"}', - }, - { - label: 'Dec 16, 2023 to Jan 16, 2024', - value: '{"from":"2023-12-16T00:00:00.000Z","to":"2024-01-16T00:00:00.000Z"}', - }, - { - label: 'Nov 16, 2023 to Dec 16, 2023', - value: '{"from":"2023-11-16T00:00:00.000Z","to":"2023-12-16T00:00:00.000Z"}', - }, - { - label: 'Oct 16, 2023 to Nov 16, 2023', - value: '{"from":"2023-10-16T00:00:00.000Z","to":"2023-11-16T00:00:00.000Z"}', - }, - { - label: 'Sep 16, 2023 to Oct 16, 2023', - value: '{"from":"2023-09-16T00:00:00.000Z","to":"2023-10-16T00:00:00.000Z"}', - }, - { - label: 'Aug 16, 2023 to Sep 16, 2023', - value: '{"from":"2023-08-16T00:00:00.000Z","to":"2023-09-16T00:00:00.000Z"}', - }, - { - label: 'Jul 16, 2023 to Aug 16, 2023', - value: '{"from":"2023-07-16T00:00:00.000Z","to":"2023-08-16T00:00:00.000Z"}', - }, - { - label: 'Jun 16, 2023 to Jul 16, 2023', - value: '{"from":"2023-06-16T00:00:00.000Z","to":"2023-07-16T00:00:00.000Z"}', - }, - ]) - }) - - it('should compute periods given a renewal date with a day in month before creation date', () => { - const organization = organizationFactoryMock(1)[0] - organization.created_at = '2023-06-16T14:34:04Z' - const orgRenewalAt = '2023-07-10T14:34:04Z' - expect(getReportPeriods({ organization, orgRenewalAt })).toEqual([ - { - label: 'Apr 10, 2024 to now', - value: '{"from":"2024-04-10T00:00:00.000Z"}', - }, - { - label: 'Mar 10, 2024 to Apr 10, 2024', - value: '{"from":"2024-03-10T00:00:00.000Z","to":"2024-04-10T00:00:00.000Z"}', - }, - { - label: 'Feb 10, 2024 to Mar 10, 2024', - value: '{"from":"2024-02-10T00:00:00.000Z","to":"2024-03-10T00:00:00.000Z"}', - }, - { - label: 'Jan 10, 2024 to Feb 10, 2024', - value: '{"from":"2024-01-10T00:00:00.000Z","to":"2024-02-10T00:00:00.000Z"}', - }, - { - label: 'Dec 10, 2023 to Jan 10, 2024', - value: '{"from":"2023-12-10T00:00:00.000Z","to":"2024-01-10T00:00:00.000Z"}', - }, - { - label: 'Nov 10, 2023 to Dec 10, 2023', - value: '{"from":"2023-11-10T00:00:00.000Z","to":"2023-12-10T00:00:00.000Z"}', - }, - { - label: 'Oct 10, 2023 to Nov 10, 2023', - value: '{"from":"2023-10-10T00:00:00.000Z","to":"2023-11-10T00:00:00.000Z"}', - }, - { - label: 'Sep 10, 2023 to Oct 10, 2023', - value: '{"from":"2023-09-10T00:00:00.000Z","to":"2023-10-10T00:00:00.000Z"}', - }, - { - label: 'Aug 10, 2023 to Sep 10, 2023', - value: '{"from":"2023-08-10T00:00:00.000Z","to":"2023-09-10T00:00:00.000Z"}', - }, - { - label: 'Jul 10, 2023 to Aug 10, 2023', - value: '{"from":"2023-07-10T00:00:00.000Z","to":"2023-08-10T00:00:00.000Z"}', - }, - ]) - }) - - it('should compute periods given a renewal date with a day in month after creation date', () => { - const organization = organizationFactoryMock(1)[0] - organization.created_at = '2023-06-16T14:34:04Z' - const orgRenewalAt = '2023-07-21T14:34:04Z' - expect(getReportPeriods({ organization, orgRenewalAt })).toEqual([ - { - label: 'Apr 21, 2024 to now', - value: '{"from":"2024-04-21T00:00:00.000Z"}', - }, - { - label: 'Mar 21, 2024 to Apr 21, 2024', - value: '{"from":"2024-03-21T00:00:00.000Z","to":"2024-04-21T00:00:00.000Z"}', - }, - { - label: 'Feb 21, 2024 to Mar 21, 2024', - value: '{"from":"2024-02-21T00:00:00.000Z","to":"2024-03-21T00:00:00.000Z"}', - }, - { - label: 'Jan 21, 2024 to Feb 21, 2024', - value: '{"from":"2024-01-21T00:00:00.000Z","to":"2024-02-21T00:00:00.000Z"}', - }, - { - label: 'Dec 21, 2023 to Jan 21, 2024', - value: '{"from":"2023-12-21T00:00:00.000Z","to":"2024-01-21T00:00:00.000Z"}', - }, - { - label: 'Nov 21, 2023 to Dec 21, 2023', - value: '{"from":"2023-11-21T00:00:00.000Z","to":"2023-12-21T00:00:00.000Z"}', - }, - { - label: 'Oct 21, 2023 to Nov 21, 2023', - value: '{"from":"2023-10-21T00:00:00.000Z","to":"2023-11-21T00:00:00.000Z"}', - }, - { - label: 'Sep 21, 2023 to Oct 21, 2023', - value: '{"from":"2023-09-21T00:00:00.000Z","to":"2023-10-21T00:00:00.000Z"}', - }, - { - label: 'Aug 21, 2023 to Sep 21, 2023', - value: '{"from":"2023-08-21T00:00:00.000Z","to":"2023-09-21T00:00:00.000Z"}', - }, - { - label: 'Jul 21, 2023 to Aug 21, 2023', - value: '{"from":"2023-07-21T00:00:00.000Z","to":"2023-08-21T00:00:00.000Z"}', - }, - { - label: 'Jun 21, 2023 to Jul 21, 2023', - value: '{"from":"2023-06-21T00:00:00.000Z","to":"2023-07-21T00:00:00.000Z"}', - }, - ]) - }) - - it('should compute periods given a renewal date next year', () => { - const organization = organizationFactoryMock(1)[0] - organization.created_at = '2023-12-16T14:34:04Z' - const orgRenewalAt = '2024-01-21T14:34:04Z' - expect(getReportPeriods({ organization, orgRenewalAt })).toEqual([ - { - label: 'Apr 21, 2024 to now', - value: '{"from":"2024-04-21T00:00:00.000Z"}', - }, - { - label: 'Mar 21, 2024 to Apr 21, 2024', - value: '{"from":"2024-03-21T00:00:00.000Z","to":"2024-04-21T00:00:00.000Z"}', - }, - { - label: 'Feb 21, 2024 to Mar 21, 2024', - value: '{"from":"2024-02-21T00:00:00.000Z","to":"2024-03-21T00:00:00.000Z"}', - }, - { - label: 'Jan 21, 2024 to Feb 21, 2024', - value: '{"from":"2024-01-21T00:00:00.000Z","to":"2024-02-21T00:00:00.000Z"}', - }, - { - label: 'Dec 21, 2023 to Jan 21, 2024', - value: '{"from":"2023-12-21T00:00:00.000Z","to":"2024-01-21T00:00:00.000Z"}', - }, - ]) - }) -}) diff --git a/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-value-modal/show-usage-value-modal.spec.tsx b/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-value-modal/show-usage-value-modal.spec.tsx index 11054d36c21..54d94809dbc 100644 --- a/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-value-modal/show-usage-value-modal.spec.tsx +++ b/libs/domains/organizations/feature/src/lib/settings-billing-summary/show-usage-modal-feature/show-usage-value-modal/show-usage-value-modal.spec.tsx @@ -1,4 +1,3 @@ -import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' import { renderWithProviders, screen } from '@qovery/shared/util-tests' import { ShowUsageValueModal, type ShowUsageValueModalProps } from './show-usage-value-modal' @@ -10,17 +9,24 @@ const props: ShowUsageValueModalProps = { describe('ShowUsageValueModal', () => { it('should render successfully', () => { - const { baseElement } = renderWithProviders(wrapWithReactHookForm()) + const { baseElement } = renderWithProviders() expect(baseElement).toBeTruthy() }) it('should render copy paste widget', () => { - renderWithProviders(wrapWithReactHookForm()) + renderWithProviders() screen.getByTestId('copy-container') }) it('should render token value', () => { - renderWithProviders(wrapWithReactHookForm()) + renderWithProviders() screen.getByDisplayValue(props.url) }) + + it('should display the expiration time', () => { + renderWithProviders() + screen.getByText((content) => + content.replace(/\s+/g, ' ').includes(`This link expires in ${props.url_expires_in_hours} hours.`) + ) + }) }) diff --git a/libs/pages/settings/src/lib/feature/page-organization-billing-summary-feature/promo-code-modal-feature/promo-code-modal-feature.spec.tsx b/libs/pages/settings/src/lib/feature/page-organization-billing-summary-feature/promo-code-modal-feature/promo-code-modal-feature.spec.tsx deleted file mode 100644 index d7ec591e353..00000000000 --- a/libs/pages/settings/src/lib/feature/page-organization-billing-summary-feature/promo-code-modal-feature/promo-code-modal-feature.spec.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as organizationsDomain from '@qovery/domains/organizations/feature' -import { renderWithProviders, screen } from '@qovery/shared/util-tests' -import PromoCodeModalFeature, { type PromocodeModalFeatureProps } from './promo-code-modal-feature' - -const useAddCreditCodeSpy = jest.spyOn(organizationsDomain, 'useAddCreditCode') as jest.Mock - -const props: PromocodeModalFeatureProps = { - closeModal: jest.fn(), - organizationId: '1', -} - -describe('PromoCodeModalFeature', () => { - beforeEach(() => { - useAddCreditCodeSpy.mockReturnValue({ - mutateAsync: jest.fn(), - }) - }) - - it('should render successfully', () => { - const { baseElement } = renderWithProviders() - expect(baseElement).toBeTruthy() - }) - - it('should useAddCreditCode with good params', async () => { - const { userEvent } = renderWithProviders() - - const input = screen.getByLabelText('Promo code') - await userEvent.type(input, 'test') - - const button = screen.getByTestId('submit-button') - await userEvent.click(button) - - expect(useAddCreditCodeSpy().mutateAsync).toHaveBeenCalledWith({ organizationId: '1', code: 'test' }) - }) -}) diff --git a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/page-organization-billing-summary.tsx b/libs/pages/settings/src/lib/ui/page-organization-billing-summary/page-organization-billing-summary.tsx deleted file mode 100644 index 2902bde7347..00000000000 --- a/libs/pages/settings/src/lib/ui/page-organization-billing-summary/page-organization-billing-summary.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { format } from 'date-fns' -import { type CreditCard, type OrganizationCurrentCost, PlanEnum } from 'qovery-typescript-axios' -import { useMemo } from 'react' -import { type CardImages } from 'react-payment-inputs/images' -import { useParams } from 'react-router-dom' -import { useUserSignUp } from '@qovery/domains/users-sign-up/feature' -import { SETTINGS_BILLING_URL, SETTINGS_URL } from '@qovery/shared/routes' -import { - Button, - Callout, - ExternalLink, - Heading, - Icon, - Link, - Section, - Skeleton, - imagesCreditCart, -} from '@qovery/shared/ui' -import { dateToFormat } from '@qovery/shared/util-dates' -import { costToHuman, formatPlanDisplay, pluralize } from '@qovery/shared/util-js' -import InvoicesListFeature from '../../feature/page-organization-billing-summary-feature/invoices-list-feature/invoices-list-feature' - -export interface PageOrganizationBillingSummaryProps { - currentCost?: OrganizationCurrentCost - creditCard?: CreditCard - creditCardLoading?: boolean - hasCreditCard?: boolean - onPromoCodeClick?: () => void - onShowUsageClick?: () => void - onChangePlanClick?: () => void - onCancelTrialClick?: () => void - onAddCreditCardClick?: () => void -} - -// This function is used to get the billing recurrence word to display based on the renewal date. -// it's not so accurate, but it's a good enough approximation for now -function getBillingRecurrenceStr(renewalAt: string | null | undefined): string { - if (renewalAt === null || renewalAt === undefined) return 'month' - - const now = new Date() - const renewalDate = new Date(renewalAt) - // if the renewal date is in less than 1 month, we display "month" - - if (renewalDate.getTime() - now.getTime() > 30 * 24 * 60 * 60 * 1000) return 'year' - - return 'month' -} - -export function PageOrganizationBillingSummary(props: PageOrganizationBillingSummaryProps) { - const { organizationId = '' } = useParams() - const { data: userSignUp } = useUserSignUp() - - // Get the billing recurrence word to display based on the renewal date. - // It's not so accurate, but it's a good enough approximation for now - const billingRecurrence = getBillingRecurrenceStr(props.currentCost?.renewal_at) - const remainingTrialDay = props.currentCost?.remaining_trial_day ?? 0 - const showTrialCallout = remainingTrialDay !== undefined && remainingTrialDay > 0 && !props.creditCardLoading - const showErrorCallout = (props.hasCreditCard ?? Boolean(props.creditCard)) || userSignUp?.dx_auth - - // This function is used to get the trial start date based on the remaining trial days from the API - const trialStartDate = useMemo(() => { - const remainingTrialDayFromApi = props.currentCost?.remaining_trial_day - if (remainingTrialDayFromApi === undefined || remainingTrialDayFromApi === null) return null - - const trialDurationDays = 14 - const today = new Date() - today.setHours(0, 0, 0, 0) - - const daysUntilExpiration = remainingTrialDayFromApi + 1 - const expirationDate = new Date(today) - expirationDate.setDate(expirationDate.getDate() + daysUntilExpiration) - - const startDate = new Date(expirationDate) - startDate.setDate(startDate.getDate() - trialDurationDays) - startDate.setHours(0, 0, 0, 0) - - return startDate - }, [props.currentCost?.remaining_trial_day]) - - return ( -
-
- {showTrialCallout && ( - - - - {/* Add + 1 because Chargebee return 0 when the trial is ending today */} - {showErrorCallout - ? `Your free trial plan expires ${remainingTrialDay + 1} ${pluralize(remainingTrialDay + 1, 'day')} from now` - : `No credit card registered, your account will be blocked at the end your trial in ${remainingTrialDay + 1} ${pluralize(remainingTrialDay + 1, 'day')}`} - - {showErrorCallout ? ( - <> - You have contracted a free 14-days trial on{' '} - {trialStartDate ? format(trialStartDate, 'MMMM d, yyyy') : '...'}. At the end of this plan your user - subscription will start. You cancel your trial by deleting your organization. - - ) : ( - <>Add a payment method to avoid service interruption at the end of your trial. - )} - - - - )} -
- Plan details -
- - - -
-
- -
-
-
Current plan
-
- -
{formatPlanDisplay(props.currentCost?.plan)}
-
-
- - See details - -
-
-
Current bill
-
- -
- - {costToHuman(props.currentCost?.cost?.total || 0, props.currentCost?.cost?.currency_code || 'USD')} - {' '} - / {billingRecurrence} -
-
-
- {props.currentCost?.plan !== PlanEnum.FREE && ( -

- Next invoice:{' '} - - {props.currentCost?.renewal_at && dateToFormat(props.currentCost?.renewal_at, 'MMM dd, Y')} - -

- )} -
- - {props.currentCost?.plan !== PlanEnum.FREE && ( -
-
Payment method
-
- -
- {props.creditCard ? ( - <> - - - **** {props.creditCard?.last_digit} - - - ) : ( - No credit card provided - )} -
-
-
- - Edit payment - -
- )} -
- -
-
- ) -} - -export default PageOrganizationBillingSummary diff --git a/libs/shared/console-shared/src/lib/settings-heading/settings-heading.tsx b/libs/shared/console-shared/src/lib/settings-heading/settings-heading.tsx index 1467da76453..8316d21a9dd 100644 --- a/libs/shared/console-shared/src/lib/settings-heading/settings-heading.tsx +++ b/libs/shared/console-shared/src/lib/settings-heading/settings-heading.tsx @@ -6,16 +6,17 @@ export interface SettingsHeadingProps { title: string description?: ReactNode children?: ReactNode + showNeedHelp?: boolean } -export function SettingsHeading({ title, description, children }: SettingsHeadingProps) { +export function SettingsHeading({ title, description, children, showNeedHelp = true }: SettingsHeadingProps) { return (
{title} {description &&

{description}

} - + {showNeedHelp && }{' '}
{children} diff --git a/libs/shared/ui/src/lib/components/inputs/input-select-small/input-select-small.tsx b/libs/shared/ui/src/lib/components/inputs/input-select-small/input-select-small.tsx index 9ec6c7b6402..cccaaa89ce4 100644 --- a/libs/shared/ui/src/lib/components/inputs/input-select-small/input-select-small.tsx +++ b/libs/shared/ui/src/lib/components/inputs/input-select-small/input-select-small.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react' import { type Value } from '@qovery/shared/interfaces' import { twMerge } from '@qovery/shared/util-js' import Icon from '../../icon/icon' -import { IconAwesomeEnum } from '../../icon/icon-awesome.enum' export interface InputSelectSmallProps { name: string @@ -67,7 +66,7 @@ export function InputSelectSmall(props: InputSelectSmallProps) { ))}
From 9a2b8df5c825cb8e5e9552e79762b39b88ff2b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Grandin?= Date: Wed, 18 Feb 2026 08:52:03 +0100 Subject: [PATCH 3/3] fix(tests): update snapshots for values-override-arguments-setting, page-settings-dockerfile-feature, and page-settings-resources to reflect UI changes --- ...es-override-arguments-setting.spec.tsx.snap | 18 +++++++++--------- ...e-settings-dockerfile-feature.spec.tsx.snap | 3 ++- .../page-settings-resources.spec.tsx.snap | 3 ++- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/libs/domains/service-helm/feature/src/lib/values-override-arguments-setting/__snapshots__/values-override-arguments-setting.spec.tsx.snap b/libs/domains/service-helm/feature/src/lib/values-override-arguments-setting/__snapshots__/values-override-arguments-setting.spec.tsx.snap index 90fae1e1a78..aaad23466c4 100644 --- a/libs/domains/service-helm/feature/src/lib/values-override-arguments-setting/__snapshots__/values-override-arguments-setting.spec.tsx.snap +++ b/libs/domains/service-helm/feature/src/lib/values-override-arguments-setting/__snapshots__/values-override-arguments-setting.spec.tsx.snap @@ -108,9 +108,9 @@ exports[`ValuesOverrideArgumentsSetting should match snapshot 1`] = ` --set-json -
-
-

The Dockerfile allows to package your application with the right CLIs/Libraries and as well define the command to run during its execution. The Dockerfile can be stored in your git repository or on the Qovery control plane (Raw).

@@ -42,6 +42,7 @@ exports[`PageSettingsDockerfileFeature should match snapshot 1`] = ` Need help here? +

diff --git a/libs/pages/application/src/lib/ui/page-settings-resources/__snapshots__/page-settings-resources.spec.tsx.snap b/libs/pages/application/src/lib/ui/page-settings-resources/__snapshots__/page-settings-resources.spec.tsx.snap index d5a975abacd..cdf7f1f2dd4 100644 --- a/libs/pages/application/src/lib/ui/page-settings-resources/__snapshots__/page-settings-resources.spec.tsx.snap +++ b/libs/pages/application/src/lib/ui/page-settings-resources/__snapshots__/page-settings-resources.spec.tsx.snap @@ -23,7 +23,7 @@ exports[`PageSettingsResources should render warning box and icon for cpu 1`] = Resources

Manage the resources assigned to the service.

@@ -38,6 +38,7 @@ exports[`PageSettingsResources should render warning box and icon for cpu 1`] = Need help here? +