Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion specs/owners.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"owners": ["attendees", "payments"]
"owners": ["attendees", "payments", "servicing"]
}
48 changes: 48 additions & 0 deletions specs/servicing/hold-and-cost.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@story:servicing.hold-and-cost
@owner:servicing @risk:medium
@actor:organiser
@edition:managed @edition:self-hosted
Feature: An organiser holds and costs a service event
A service event reserves listing capacity without creating a customer ticket.
The organiser can see, duplicate, delete, and cost the hold.

@rule:servicing.hold-is-visible-and-private
Rule: A service hold appears to the organiser but not to the customer
The hold reserves capacity and never becomes a public listing or ticket.

@case:servicing.hold-on-dashboard
Scenario: The organiser sees a new hold on the dashboard
Given an organiser has created a Boiler Service hold on Room A
Then the admin dashboard shows the Boiler Service hold
And the public site does not show Boiler Service

@rule:servicing.hold-can-be-duplicated
Rule: A duplicated hold is an independent copy
Duplicating a service event creates a second event with the same bookings.

@case:servicing.duplicate-hold
Scenario: The organiser duplicates an annual inspection
Given an organiser has created an Annual Inspection hold on Annual Room
When the organiser duplicates the service event
Then the admin dashboard shows two Annual Inspection holds

@rule:servicing.hold-can-be-deleted
Rule: Deleting a hold frees the reserved capacity
The hold and its booking rows are removed.

@case:servicing.delete-hold
Scenario: The organiser deletes a Boiler Service hold
Given an organiser has created a Boiler Service hold on Room A
When the organiser deletes the service event
Then the admin dashboard no longer shows Boiler Service
And the held listing has its full capacity restored

@rule:servicing.hold-can-be-costed
Rule: The organiser can record a cost against a hold
A cost is money spent on the service, recorded against one of the held listings.

@case:servicing.record-cost
Scenario: The organiser records a cost for a Boiler Service
Given an organiser has created a Boiler Service hold on Room A
When the organiser records a cost of 90.00 for Boiler Service
Then the service event page shows the recorded cost
137 changes: 0 additions & 137 deletions test/e2e/servicing/e2e-narrative.test.ts

This file was deleted.

28 changes: 28 additions & 0 deletions test/integration/servicing/route-guards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ import { createTestListing } from "#test-utils/db-helpers/listings.ts";
import {
adminPost,
assertAdmin404,
assertRedirectPathname,
assertServicingId404sEverywhere,
createRealAttendee,
createServicingHold,
createTestServicingEvent,
getServicingEvent,
listingCostOf,
recordServiceCost,
} from "#test-utils/servicing.ts";
Expand Down Expand Up @@ -83,12 +85,38 @@ describeWithEnv(
response.body?.cancel();
});

test("POST /admin/servicing/:id/delete redirects to the dashboard on success", async () => {
const { id } = await createServicingHold();
const response = await adminPost(`/admin/servicing/${id}/delete`, {});
assertRedirectPathname(response, "/admin/");
expect(await getServicingEvent(id)).toBeNull();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test("POST /admin/servicing/:id/duplicate 404s for a missing service event id", async () => {
const response = await adminPost("/admin/servicing/999999/duplicate", {});
expect(response.status).toBe(404);
response.body?.cancel();
});

test("POST /admin/servicing/:id/duplicate redirects to the copy on success", async () => {
const { id, listing } = await createServicingHold({ name: "Original" });
const response = await adminPost(`/admin/servicing/${id}/duplicate`, {});
expect(response.status).toBe(302);
const location = response.headers.get("location");
expect(location).not.toBeNull();
const match = location!.match(/\/admin\/servicing\/(\d+)/);
Comment thread
stefan-burke marked this conversation as resolved.
expect(match).not.toBeNull();
const copyId = Number(match![1]);
expect(copyId).toBeGreaterThan(id);
assertRedirectPathname(response, `/admin/servicing/${copyId}`);
const copy = await getServicingEvent(copyId);
expect(copy).not.toBeNull();
expect(copy!.name).toBe("Original");
const firstBooking = copy!.bookings[0];
expect(firstBooking).toBeDefined();
expect(firstBooking!.listingId).toBe(listing.id);
});

test("POST /admin/servicing/:id/cost/:costId 404s for a missing cost id", async () => {
const { id, listing } = await createServicingHold();
const response = await adminPost(`/admin/servicing/${id}/cost/999999`, {
Expand Down
1 change: 1 addition & 0 deletions test/scripts/specs/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe("Cucumber story catalog", () => {
expect((await readSpecCatalog()).stories.map(({ id }) => id)).toEqual([
"attendees.no-quantity-tickets",
"payments.capacity-after-payment",
"servicing.hold-and-cost",
]);
});

Expand Down
Loading