Owner Administration is complete. Summary of this final stretch of work:

Seed data extended with an inactive staff member, a pending invitation, a redeemed-reward fixture, a service with no eligible staff, and a future-appointment fixture for conflict testing — all idempotent, verified against the dev DB.

Testing brought to 179 unit / 86 integration / 27 E2E tests, all passing together on a freshly seeded, disposable Postgres:

Unit tests for every new pure validator (business hours, categories/services, booking rules, loyalty config, staff creation).
Integration tests covering opening-hours conflicts, last-owner protection, staff deactivation/eligibility conflicts, loyalty redemption's atomic race-guard, and owner-vs-barber/business-isolation authorization — all against a real Postgres.
12 new Playwright owner journeys, which surfaced and fixed two genuine issues along the way: a cookie-clearing race condition in the sign-out-mid-test flow (a real, reproducible timing bug, not just a test artifact), and confirmation that seed fixtures upserted by deterministic ID go stale once "today" has moved on — now handled defensively.
Desktop responsiveness: converted the single-column Business Details, Booking Rules, and Loyalty Configuration forms to a genuine two-column layout at the lg: breakpoint (verified via measured element positions, not just screenshots), confirmed mobile stacking is unaffected.

Final verification: clean tsc, eslint, and next build, plus the full three-suite run against one fresh database as the authoritative pass.
This commit is contained in:
Stefan Lazic committed 2026-08-03 12:56:55 +01:00
1 parent ffee9d6754
commit ec5583f972
73 files changed
+8400 -53

No files matched your search

+121
View File
@@ -0,0 +1,121 @@
"use server";
import { auth } from "@/auth";
import { prisma } from "@/lib/db/prisma";
import { Permission } from "@/generated/prisma/enums";
import { assertOwnerPermission, OwnerPermissionDeniedError } from "@/features/owner/authorization";
import { writeAuditLog } from "@/lib/audit/write-audit-log";
import {
setOpeningHours,
updateBusinessProfile,
updateBusinessSettings,
} from "@/services/business/business.service";
import type { BusinessLocationInput, BusinessProfileInput, BusinessSettingsInput, OpeningHoursDayInput } from "@/services/business/business.service";
export interface BusinessActionResult {
success: boolean;
error?: string;
}
async function requireBusinessManager() {
const session = await auth();
if (!session?.user) return { staff: null, error: "You must be signed in." } as const;
try {
await assertOwnerPermission(session.user.id, Permission.MANAGE_BUSINESS_PROFILE);
} catch (error) {
if (error instanceof OwnerPermissionDeniedError) return { staff: null, error: error.message } as const;
throw error;
}
const staff = await prisma.staffProfile.findUnique({ where: { userId: session.user.id } });
if (!staff) return { staff: null, error: "You must be signed in as staff." } as const;
return { staff, error: null } as const;
}
export async function updateBusinessProfileAction(params: {
profile: BusinessProfileInput;
location: BusinessLocationInput;
}): Promise<BusinessActionResult> {
const { staff, error } = await requireBusinessManager();
if (!staff) return { success: false, error: error ?? "Not authorized." };
const result = await updateBusinessProfile(prisma, { businessId: staff.businessId, ...params });
if (!result.success) {
return { success: false, error: result.validation.issues[0]?.message ?? "Couldn't save business details." };
}
await writeAuditLog(prisma, {
actorUserId: staff.userId,
action: "BUSINESS_PROFILE_UPDATED",
entityType: "Business",
entityId: staff.businessId,
metadata: { name: params.profile.name },
});
return { success: true };
}
export async function updateBusinessSettingsAction(settings: BusinessSettingsInput): Promise<BusinessActionResult> {
const { staff, error } = await requireBusinessManager();
if (!staff) return { success: false, error: error ?? "Not authorized." };
await updateBusinessSettings(prisma, { businessId: staff.businessId, settings });
await writeAuditLog(prisma, {
actorUserId: staff.userId,
action: "BUSINESS_SETTINGS_UPDATED",
entityType: "BusinessSettings",
entityId: staff.businessId,
});
return { success: true };
}
export interface OpeningHoursActionResult {
success: boolean;
error?: string;
conflicts?: { id: string; customerName: string; serviceNames: string; startAt: string; staffName: string }[];
}
export async function updateOpeningHoursAction(
days: OpeningHoursDayInput[],
acknowledgeConflicts?: boolean,
): Promise<OpeningHoursActionResult> {
const { staff, error } = await requireBusinessManager();
if (!staff) return { success: false, error: error ?? "Not authorized." };
const business = await prisma.business.findUnique({ where: { id: staff.businessId }, select: { timezone: true } });
if (!business) return { success: false, error: "Business not found." };
const result = await setOpeningHours(prisma, {
businessId: staff.businessId,
businessTimezone: business.timezone,
days,
acknowledgeConflicts,
});
if (!result.success) {
return {
success: false,
error: result.validation.issues[0]?.message ?? "Couldn't save opening hours.",
conflicts: result.conflicts.map((c) => ({
id: c.id,
customerName: `${c.customer.firstName} ${c.customer.lastName}`,
serviceNames: c.services.map((s) => s.service.name).join(" + "),
startAt: c.startAt.toISOString(),
staffName: c.staff.user.name ?? "Staff",
})),
};
}
await writeAuditLog(prisma, {
actorUserId: staff.userId,
action: "BUSINESS_HOURS_UPDATED",
entityType: "BusinessHours",
entityId: staff.businessId,
});
return { success: true };
}