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

@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import { validateProgrammeConfig } from "../loyalty.service";
import type { ProgrammeConfigInput } from "../loyalty.service";
const base: ProgrammeConfigInput = {
isEnabled: true,
stampsRequired: 10,
rewardType: "PERCENTAGE",
rewardValue: "50",
autoApplyReward: true,
requiresStaffApproval: false,
earnOnCancelled: false,
earnOnNoShow: false,
};
describe("validateProgrammeConfig", () => {
it("accepts a valid percentage-reward configuration", () => {
expect(validateProgrammeConfig(base).valid).toBe(true);
});
it("rejects a stamps-required of zero", () => {
const result = validateProgrammeConfig({ ...base, stampsRequired: 0 });
expect(result.valid).toBe(false);
expect(result.issues[0]?.code).toBe("INVALID_INPUT");
});
it("allows an incomplete configuration while disabled", () => {
const result = validateProgrammeConfig({ ...base, isEnabled: false, rewardValue: undefined });
expect(result.valid).toBe(true);
});
it.each([0, 101, -5])("rejects an out-of-range percentage when enabled (%s)", (pct) => {
const result = validateProgrammeConfig({ ...base, rewardValue: String(pct) });
expect(result.valid).toBe(false);
expect(result.issues[0]?.code).toBe("INVALID_PERCENTAGE");
});
it("rejects a missing percentage value when enabled", () => {
const result = validateProgrammeConfig({ ...base, rewardValue: undefined });
expect(result.valid).toBe(false);
expect(result.issues[0]?.code).toBe("INVALID_PERCENTAGE");
});
it("accepts a valid fixed-amount reward", () => {
const result = validateProgrammeConfig({ ...base, rewardType: "FIXED_AMOUNT", rewardValue: "10.00" });
expect(result.valid).toBe(true);
});
it("rejects a negative fixed-amount reward", () => {
const result = validateProgrammeConfig({ ...base, rewardType: "FIXED_AMOUNT", rewardValue: "-1" });
expect(result.valid).toBe(false);
expect(result.issues[0]?.code).toBe("INVALID_VALUE");
});
it("accepts a free-service reward with a service chosen", () => {
const result = validateProgrammeConfig({ ...base, rewardType: "FREE_SERVICE", freeServiceId: "svc-1" });
expect(result.valid).toBe(true);
});
it("rejects a free-service reward with no service chosen", () => {
const result = validateProgrammeConfig({ ...base, rewardType: "FREE_SERVICE", freeServiceId: undefined });
expect(result.valid).toBe(false);
expect(result.issues[0]?.code).toBe("FREE_SERVICE_REQUIRED");
});
});