Fix endcustomer package allocation and event create gating
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-06 13:21:11 +01:00
parent 0291d537fb
commit df00deb0df
11 changed files with 409 additions and 14 deletions

View File

@@ -90,6 +90,20 @@ function resellerHasRemainingEvents(pkg: TenantPackageSummary): boolean {
return limitMaxEvents > usedEvents;
}
function endcustomerHasRemainingEvents(pkg: TenantPackageSummary): boolean {
if (pkg.package_type !== 'endcustomer') {
return false;
}
const linkedEvents = toNumber(pkg.linked_events_count) ?? 0;
return linkedEvents < 1;
}
function packageHasRemainingEvents(pkg: TenantPackageSummary): boolean {
return resellerHasRemainingEvents(pkg) || endcustomerHasRemainingEvents(pkg);
}
// --- TAMAGUI-ALIGNED PRIMITIVES ---
function DashboardCard({
@@ -252,7 +266,7 @@ export default function MobileDashboardPage() {
}
const activePackages = collectActivePackages(packagesOverview ?? null);
return activePackages.some((pkg) => resellerHasRemainingEvents(pkg));
return activePackages.some((pkg) => packageHasRemainingEvents(pkg));
}, [canManageEvents, isSuperAdmin, isMember, packagesLoading, packagesOverview]);
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';

View File

@@ -99,7 +99,7 @@ export default function MobileEventsPage() {
}
const activePackages = collectActivePackages(packagesOverview);
return activePackages.some((pkg) => resellerHasRemainingEvents(pkg));
return activePackages.some((pkg) => packageHasRemainingEvents(pkg));
}, [isMember, isSuperAdmin, packagesLoading, packagesOverview]);
return (
@@ -570,6 +570,20 @@ function resellerHasRemainingEvents(pkg: TenantPackageSummary): boolean {
return limitMaxEvents > usedEvents;
}
function endcustomerHasRemainingEvents(pkg: TenantPackageSummary): boolean {
if (pkg.package_type !== 'endcustomer') {
return false;
}
const linkedEvents = toNumber(pkg.linked_events_count) ?? 0;
return linkedEvents < 1;
}
function packageHasRemainingEvents(pkg: TenantPackageSummary): boolean {
return resellerHasRemainingEvents(pkg) || endcustomerHasRemainingEvents(pkg);
}
function resolveEventSearchName(name: TenantEvent['name'], t: (key: string) => string): string {
if (typeof name === 'string') return name;
if (name && typeof name === 'object') {

View File

@@ -79,6 +79,7 @@ vi.mock('../../api', () => ({
purchased_at: null,
expires_at: null,
package_limits: null,
linked_events_count: 0,
},
],
activePackage: {
@@ -95,6 +96,7 @@ vi.mock('../../api', () => ({
purchased_at: null,
expires_at: null,
package_limits: null,
linked_events_count: 0,
},
}),
}));
@@ -295,6 +297,49 @@ describe('MobileEventsPage', () => {
expect(await screen.findByText('New event')).toBeInTheDocument();
});
it('shows the create button for available endcustomer packages', async () => {
vi.mocked(api.getTenantPackagesOverview).mockResolvedValueOnce({
packages: [
{
id: 3,
package_id: 3,
package_name: 'Classic',
package_type: 'endcustomer',
included_package_slug: null,
active: true,
used_events: 0,
remaining_events: null,
price: 120,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: null,
linked_events_count: 0,
},
],
activePackage: {
id: 3,
package_id: 3,
package_name: 'Classic',
package_type: 'endcustomer',
included_package_slug: null,
active: true,
used_events: 0,
remaining_events: null,
price: 120,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: null,
linked_events_count: 0,
},
});
render(<MobileEventsPage />);
expect(await screen.findByText('New event')).toBeInTheDocument();
});
it('hides the create button when no remaining events are available', async () => {
vi.mocked(api.getTenantPackagesOverview).mockResolvedValueOnce({
packages: [
@@ -336,4 +381,48 @@ describe('MobileEventsPage', () => {
expect(await screen.findByText('Demo Event')).toBeInTheDocument();
expect(screen.queryByText('New event')).not.toBeInTheDocument();
});
it('hides the create button for consumed endcustomer packages', async () => {
vi.mocked(api.getTenantPackagesOverview).mockResolvedValueOnce({
packages: [
{
id: 4,
package_id: 4,
package_name: 'Classic',
package_type: 'endcustomer',
included_package_slug: null,
active: true,
used_events: 0,
remaining_events: null,
price: 120,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: null,
linked_events_count: 1,
},
],
activePackage: {
id: 4,
package_id: 4,
package_name: 'Classic',
package_type: 'endcustomer',
included_package_slug: null,
active: true,
used_events: 0,
remaining_events: null,
price: 120,
currency: 'EUR',
purchased_at: null,
expires_at: null,
package_limits: null,
linked_events_count: 1,
},
});
render(<MobileEventsPage />);
expect(await screen.findByText('Demo Event')).toBeInTheDocument();
expect(screen.queryByText('New event')).not.toBeInTheDocument();
});
});

View File

@@ -85,6 +85,20 @@ function resellerHasRemainingEvents(pkg: TenantPackageSummary): boolean {
return limitMaxEvents > usedEvents;
}
function endcustomerHasRemainingEvents(pkg: TenantPackageSummary): boolean {
if (pkg.package_type !== 'endcustomer') {
return false;
}
const linkedEvents = toNumber(pkg.linked_events_count) ?? 0;
return linkedEvents < 1;
}
function packageHasRemainingEvents(pkg: TenantPackageSummary): boolean {
return resellerHasRemainingEvents(pkg) || endcustomerHasRemainingEvents(pkg);
}
export function MobileShell({ title, subtitle, children, activeTab, onBack, headerActions }: MobileShellProps) {
const { events, activeEvent, selectEvent } = useEventContext();
const { user } = useAuth();
@@ -219,7 +233,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
}
const activePackages = collectActivePackages(packagesOverview ?? null);
return activePackages.some((pkg) => resellerHasRemainingEvents(pkg));
return activePackages.some((pkg) => packageHasRemainingEvents(pkg));
}, [isMember, isSuperAdmin, packagesLoading, packagesOverview]);
// --- CONTEXT PILL ---