feat(addons): finalize event addon catalog and ai styling upgrade flow
This commit is contained in:
@@ -153,6 +153,7 @@ vi.mock('../invite-layout/export-utils', () => ({
|
||||
|
||||
vi.mock('../../api', () => ({
|
||||
getEvent: vi.fn(),
|
||||
getAddonCatalog: vi.fn().mockResolvedValue([]),
|
||||
getTenantPackagesOverview: vi.fn().mockResolvedValue({ packages: [], activePackage: null }),
|
||||
getTenantBillingTransactions: vi.fn().mockResolvedValue({
|
||||
data: [
|
||||
@@ -199,6 +200,7 @@ describe('MobileBillingPage', () => {
|
||||
} as any);
|
||||
vi.mocked(api.getTenantAddonHistory).mockResolvedValue({ data: [] } as any);
|
||||
vi.mocked(api.getEvent).mockResolvedValue(null as any);
|
||||
vi.mocked(api.getAddonCatalog).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it('shows current event scoped entitlements separately from tenant history', async () => {
|
||||
@@ -480,4 +482,39 @@ describe('MobileBillingPage', () => {
|
||||
expect(triggerDownloadMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('offers a direct add-on purchase entry in billing for the selected event', async () => {
|
||||
eventContext.activeEvent = {
|
||||
id: 99,
|
||||
slug: 'fruehlingsfest',
|
||||
name: { de: 'Frühlingsfest' },
|
||||
};
|
||||
|
||||
vi.mocked(api.getEvent).mockResolvedValueOnce({
|
||||
id: 99,
|
||||
slug: 'fruehlingsfest',
|
||||
name: { de: 'Frühlingsfest' },
|
||||
event_date: null,
|
||||
event_type_id: null,
|
||||
event_type: null,
|
||||
status: 'published',
|
||||
settings: {},
|
||||
package: null,
|
||||
addons: [],
|
||||
limits: null,
|
||||
member_permissions: [],
|
||||
} as any);
|
||||
vi.mocked(api.getAddonCatalog).mockResolvedValueOnce([
|
||||
{
|
||||
key: 'extend_gallery_30d',
|
||||
label: 'Galerie +30 Tage',
|
||||
price_id: 'paypal',
|
||||
increments: { extra_gallery_days: 30 },
|
||||
},
|
||||
] as any);
|
||||
|
||||
render(<MobileBillingPage />);
|
||||
|
||||
expect(await screen.findByText('Buy add-ons for this event')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -219,6 +219,7 @@ vi.mock('../../api', () => ({
|
||||
}));
|
||||
|
||||
import MobileEventControlRoomPage from '../EventControlRoomPage';
|
||||
import * as api from '../../api';
|
||||
|
||||
describe('MobileEventControlRoomPage', () => {
|
||||
it('renders compact grid actions for moderation photos', async () => {
|
||||
@@ -228,4 +229,22 @@ describe('MobileEventControlRoomPage', () => {
|
||||
expect(screen.getByLabelText('Hide')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Set highlight')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows AI addon upsell when AI is locked but addon is purchasable', async () => {
|
||||
(api.getAddonCatalog as any).mockResolvedValueOnce([
|
||||
{
|
||||
key: 'ai_styling_unlock',
|
||||
label: 'AI Styling Add-on',
|
||||
price_id: 'paypal',
|
||||
increments: {},
|
||||
price: 9,
|
||||
currency: 'EUR',
|
||||
},
|
||||
]);
|
||||
|
||||
render(<MobileEventControlRoomPage />);
|
||||
|
||||
expect(await screen.findByText('Unlock AI Magic Edit')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Unlock AI Magic Edit/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
const fixtures = vi.hoisted(() => ({
|
||||
event: {
|
||||
@@ -23,7 +23,14 @@ const fixtures = vi.hoisted(() => ({
|
||||
is_active: true,
|
||||
},
|
||||
invites: [],
|
||||
addons: [],
|
||||
addons: [
|
||||
{
|
||||
key: 'extend_gallery_30d',
|
||||
label: 'Galerie um 30 Tage verlängern',
|
||||
price_id: 'price_30d',
|
||||
increments: { extra_gallery_days: 30 },
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock('react-router-dom', () => ({
|
||||
@@ -60,7 +67,11 @@ vi.mock('../../api', () => ({
|
||||
getEventQrInvites: vi.fn().mockResolvedValue(fixtures.invites),
|
||||
getAddonCatalog: vi.fn().mockResolvedValue(fixtures.addons),
|
||||
updateEvent: vi.fn().mockResolvedValue(fixtures.event),
|
||||
createEventAddonCheckout: vi.fn(),
|
||||
createEventAddonCheckout: vi.fn().mockResolvedValue({
|
||||
checkout_url: null,
|
||||
checkout_id: 'chk_123',
|
||||
expires_at: null,
|
||||
}),
|
||||
getEventEngagement: vi.fn().mockResolvedValue({
|
||||
summary: { totalPhotos: 0, uniqueGuests: 0, tasksSolved: 0, likesTotal: 0 },
|
||||
leaderboards: { uploads: [], likes: [] },
|
||||
@@ -87,7 +98,11 @@ vi.mock('../components/MobileShell', () => ({
|
||||
|
||||
vi.mock('../components/Primitives', () => ({
|
||||
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
CTAButton: ({ label }: { label: string }) => <button type="button">{label}</button>,
|
||||
CTAButton: ({ label, onPress }: { label: string; onPress?: () => void }) => (
|
||||
<button type="button" onClick={onPress}>
|
||||
{label}
|
||||
</button>
|
||||
),
|
||||
PillBadge: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SkeletonCard: () => <div>Loading...</div>,
|
||||
}));
|
||||
@@ -101,7 +116,13 @@ vi.mock('../components/FormControls', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../components/LegalConsentSheet', () => ({
|
||||
LegalConsentSheet: () => <div />,
|
||||
LegalConsentSheet: ({
|
||||
open,
|
||||
onConfirm,
|
||||
}: {
|
||||
open: boolean;
|
||||
onConfirm: (consents: { acceptedTerms: boolean; acceptedWaiver: boolean }) => void;
|
||||
}) => (open ? <button type="button" onClick={() => onConfirm({ acceptedTerms: true, acceptedWaiver: true })}>Confirm legal</button> : null),
|
||||
}));
|
||||
|
||||
vi.mock('@tamagui/stacks', () => ({
|
||||
@@ -164,8 +185,21 @@ vi.mock('../theme', () => ({
|
||||
}));
|
||||
|
||||
import MobileEventRecapPage from '../EventRecapPage';
|
||||
import { createEventAddonCheckout } from '../../api';
|
||||
|
||||
describe('MobileEventRecapPage', () => {
|
||||
beforeEach(() => {
|
||||
fixtures.addons = [
|
||||
{
|
||||
key: 'extend_gallery_30d',
|
||||
label: 'Galerie um 30 Tage verlängern',
|
||||
price_id: 'price_30d',
|
||||
increments: { extra_gallery_days: 30 },
|
||||
},
|
||||
];
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders recap settings toggles', async () => {
|
||||
render(<MobileEventRecapPage />);
|
||||
|
||||
@@ -173,4 +207,26 @@ describe('MobileEventRecapPage', () => {
|
||||
expect(screen.getByLabelText('Gäste dürfen Fotos laden')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Gäste dürfen Fotos teilen')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('requires consent and sends both legal acceptance flags for recap addon checkout', async () => {
|
||||
const checkoutMock = vi.mocked(createEventAddonCheckout);
|
||||
render(<MobileEventRecapPage />);
|
||||
|
||||
const checkoutButton = await screen.findByRole('button', { name: 'Galerie um 30 Tage verlängern' });
|
||||
fireEvent.click(checkoutButton);
|
||||
|
||||
expect(checkoutMock).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Confirm legal' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkoutMock).toHaveBeenCalledWith(fixtures.event.slug, {
|
||||
addon_key: 'extend_gallery_30d',
|
||||
success_url: window.location.href,
|
||||
cancel_url: window.location.href,
|
||||
accepted_terms: true,
|
||||
accepted_waiver: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user