Fix PayPal billing flow and mobile admin UX
This commit is contained in:
181
resources/js/admin/mobile/__tests__/BillingPage.test.tsx
Normal file
181
resources/js/admin/mobile/__tests__/BillingPage.test.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
const navigateMock = vi.fn();
|
||||
|
||||
const tMock = (
|
||||
_key: string,
|
||||
fallback?: string | Record<string, unknown>,
|
||||
options?: Record<string, unknown>,
|
||||
) => {
|
||||
let value = typeof fallback === 'string' ? fallback : _key;
|
||||
if (options) {
|
||||
Object.entries(options).forEach(([key, val]) => {
|
||||
value = value.replaceAll(`{{${key}}}`, String(val));
|
||||
});
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const downloadReceiptMock = vi.fn().mockResolvedValue(new Blob(['pdf'], { type: 'application/pdf' }));
|
||||
const triggerDownloadMock = vi.fn();
|
||||
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useNavigate: () => navigateMock,
|
||||
useLocation: () => ({ pathname: '/mobile/billing', search: '', hash: '' }),
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: tMock }),
|
||||
initReactI18next: {
|
||||
type: '3rdParty',
|
||||
init: () => undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('lucide-react', () => ({
|
||||
Package: () => <span />,
|
||||
Receipt: () => <span />,
|
||||
RefreshCcw: () => <span />,
|
||||
Sparkles: () => <span />,
|
||||
}));
|
||||
|
||||
vi.mock('react-hot-toast', () => {
|
||||
const toast = Object.assign(vi.fn(), {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
});
|
||||
return { default: toast };
|
||||
});
|
||||
|
||||
vi.mock('../hooks/useBackNavigation', () => ({
|
||||
useBackNavigation: () => vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../theme', () => ({
|
||||
useAdminTheme: () => ({
|
||||
textStrong: '#111827',
|
||||
text: '#111827',
|
||||
muted: '#6b7280',
|
||||
subtle: '#9ca3af',
|
||||
danger: '#b91c1c',
|
||||
border: '#e5e7eb',
|
||||
primary: '#2563eb',
|
||||
accentSoft: '#eef2ff',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../components/MobileShell', () => ({
|
||||
MobileShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
HeaderActionButton: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../components/Primitives', () => ({
|
||||
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
CTAButton: ({ label, onPress }: { label: string; onPress?: () => void }) => (
|
||||
<button type="button" onClick={onPress}>
|
||||
{label}
|
||||
</button>
|
||||
),
|
||||
PillBadge: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||
}));
|
||||
|
||||
vi.mock('../components/ContextHelpLink', () => ({
|
||||
ContextHelpLink: () => <div />,
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/stacks', () => ({
|
||||
YStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
XStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/text', () => ({
|
||||
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/react-native-web-lite', () => ({
|
||||
Pressable: ({
|
||||
children,
|
||||
onPress,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onPress?: () => void;
|
||||
}) => (
|
||||
<button type="button" onClick={onPress}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../lib/apiError', () => ({
|
||||
getApiErrorMessage: (_err: unknown, fallback: string) => fallback,
|
||||
}));
|
||||
|
||||
vi.mock('../../constants', () => ({
|
||||
ADMIN_EVENT_VIEW_PATH: '/mobile/events',
|
||||
adminPath: (path: string) => path,
|
||||
}));
|
||||
|
||||
vi.mock('../billingUsage', () => ({
|
||||
buildPackageUsageMetrics: () => [],
|
||||
formatPackageEventAllowance: () => '—',
|
||||
getUsageState: () => 'ok',
|
||||
usagePercent: () => 0,
|
||||
}));
|
||||
|
||||
vi.mock('../lib/packageSummary', () => ({
|
||||
collectPackageFeatures: () => [],
|
||||
formatEventUsage: () => '',
|
||||
getPackageFeatureLabel: () => '',
|
||||
getPackageLimitEntries: () => [],
|
||||
resolveTenantWatermarkFeatureKey: () => '',
|
||||
}));
|
||||
|
||||
vi.mock('../lib/billingCheckout', () => ({
|
||||
loadPendingCheckout: () => null,
|
||||
shouldClearPendingCheckout: () => false,
|
||||
storePendingCheckout: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../invite-layout/export-utils', () => ({
|
||||
triggerDownloadFromBlob: (...args: unknown[]) => triggerDownloadMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('../../api', () => ({
|
||||
getTenantPackagesOverview: vi.fn().mockResolvedValue({ packages: [], activePackage: null }),
|
||||
getTenantBillingTransactions: vi.fn().mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
status: 'completed',
|
||||
amount: 49,
|
||||
currency: 'EUR',
|
||||
provider: 'paypal',
|
||||
provider_id: 'ORDER-1',
|
||||
package_name: 'Starter',
|
||||
purchased_at: '2024-01-01T00:00:00Z',
|
||||
receipt_url: '/api/v1/billing/transactions/1/receipt',
|
||||
},
|
||||
],
|
||||
}),
|
||||
getTenantAddonHistory: vi.fn().mockResolvedValue({ data: [] }),
|
||||
getTenantPackageCheckoutStatus: vi.fn(),
|
||||
downloadTenantBillingReceipt: (...args: unknown[]) => downloadReceiptMock(...args),
|
||||
}));
|
||||
|
||||
import MobileBillingPage from '../BillingPage';
|
||||
|
||||
describe('MobileBillingPage', () => {
|
||||
it('downloads receipts via the API helper', async () => {
|
||||
render(<MobileBillingPage />);
|
||||
|
||||
const receiptLink = await screen.findByText('Beleg');
|
||||
fireEvent.click(receiptLink);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(downloadReceiptMock).toHaveBeenCalledWith('/api/v1/billing/transactions/1/receipt');
|
||||
expect(triggerDownloadMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
13
resources/js/admin/mobile/__tests__/packageSummary.test.ts
Normal file
13
resources/js/admin/mobile/__tests__/packageSummary.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getPackageLimitEntries } from '../lib/packageSummary';
|
||||
|
||||
const t = (_key: string, fallback?: string) => fallback ?? _key;
|
||||
|
||||
describe('getPackageLimitEntries', () => {
|
||||
it('defaults endcustomer event limit to 1 when missing', () => {
|
||||
const entries = getPackageLimitEntries({}, t, {}, { packageType: 'endcustomer' });
|
||||
const eventEntry = entries.find((entry) => entry.key === 'max_events_per_year');
|
||||
|
||||
expect(eventEntry?.value).toBe('1');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user