Repository navigation
Tell holidays, discount codes, and the news as Cucumber stories #2017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| @story:bookings.taking-a-holiday | ||
| @owner:bookings @risk:medium | ||
| @actor:organiser @actor:customer | ||
| @edition:managed @edition:self-hosted | ||
| @surface:admin | ||
| Feature: The organiser takes a holiday | ||
| When the organiser adds a holiday, its days disappear from every daily | ||
| listing's date choices — nobody can book a day the doors are shut. | ||
| Deleting the holiday opens the days straight back up, and things sold | ||
| for one fixed moment are never touched by it. | ||
|
|
||
| @rule:bookings.a-holiday-takes-its-days-off-the-menu | ||
| @surface:public | ||
| Rule: A holiday takes its days off the menu | ||
| Adding a holiday is confirmed to the organiser, and from then on a daily | ||
| listing's booking page offers none of the holiday's days. The days either | ||
| side stay on offer. | ||
|
|
||
| @case:holidays.the-closed-day-vanishes | ||
| Scenario: The organiser closes one day | ||
| Given the site sells day places at the Pottery | ||
| When the organiser adds a holiday called "Spring break" on the day 5 days from now | ||
| Then the organiser is told the holiday was created | ||
| And the Pottery no longer offers the day 5 days from now | ||
| But the Pottery still offers the day 4 days from now | ||
| And the Pottery still offers the day 6 days from now | ||
|
|
||
| @rule:bookings.deleting-a-holiday-reopens-its-days | ||
| @surface:public | ||
| Rule: Deleting a holiday opens its days straight back up | ||
| The moment a holiday is deleted, the days it closed are offered again — | ||
| no restart, no waiting. | ||
|
|
||
| @case:holidays.deleting-reopens-the-day | ||
| Scenario: The organiser deletes the holiday | ||
| Given the site sells day places at the Pottery | ||
| And the organiser has added a holiday called "Spring break" on the day 5 days from now | ||
| When the organiser deletes the holiday "Spring break" typing its exact name | ||
| Then the organiser is told the holiday was deleted | ||
| And the Pottery offers the day 5 days from now again | ||
|
|
||
| @rule:bookings.a-holiday-only-closes-things-sold-by-the-day | ||
| @surface:public | ||
| Rule: A holiday only closes things sold by the day | ||
| A holiday closes daily listings and nothing else. Something sold as | ||
| plain places — a gala, a show — still takes bookings right through | ||
| the holiday. | ||
|
|
||
| @case:holidays.the-gala-sells-through-the-holiday | ||
| Scenario: A customer books the Gala during the holiday | ||
| Given the site sells places at the Gala | ||
| And the organiser has added a holiday called "Deep clean" covering today and the next 7 days | ||
| When a customer books a place at the Gala | ||
| Then the Gala keeps that booking |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| @story:payments.booking-with-a-discount-code | ||
| @owner:payments @risk:high | ||
| @actor:organiser @actor:customer | ||
| @edition:managed @edition:self-hosted | ||
| @surface:public | ||
| Feature: A customer books with a discount code | ||
| The organiser hands out a code, and a customer who types it at checkout | ||
| pays less. The listing's own price never changes — the discount shows as | ||
| its own line, and only the total comes down. A code is worth nothing to | ||
| anyone who does not have it. | ||
|
|
||
| @rule:payments.the-code-box-waits-for-a-code-to-exist | ||
| Rule: The code box waits for a code to exist | ||
| Until the organiser has made a promo code, the booking page asks for no | ||
| code at all. Creating one is confirmed, and the box appears. | ||
|
|
||
| @case:discount-codes.no-codes-no-box | ||
| Scenario: Nobody has made a code yet | ||
| Given the site sells places at the Pottery for 10.00 | ||
| Then the Pottery booking page offers no promo code box | ||
|
|
||
| @case:discount-codes.the-first-code-brings-the-box | ||
| Scenario: The organiser makes the first code | ||
| Given the site sells places at the Pottery for 10.00 | ||
| When the organiser creates a promo code "SAVE10" taking 10 percent off | ||
| Then the organiser is told the modifier was created | ||
| And the Pottery booking page offers a promo code box | ||
|
|
||
| @rule:payments.the-right-code-cuts-only-the-total | ||
| Rule: The right code cuts the total, not the listing's price | ||
| The price summary keeps the place at its full price and shows the | ||
| discount as its own line, so the customer can see exactly what the code | ||
| was worth. Typing the code in the wrong case costs nothing — the code is | ||
| matched however it is typed. | ||
|
|
||
| @case:discount-codes.the-code-cuts-the-total | ||
| Scenario: A customer types the code | ||
| Given the site sells places at the Pottery for 10.00 | ||
| And the organiser has a promo code "SAVE10" taking 10 percent off | ||
| When a customer asks the price of a Pottery place with the code "SAVE10" | ||
| Then the summary shows the place at 10.00 | ||
| And the summary shows the discount line "SAVE10" taking off 1.00 | ||
| And the summary total is 9.00 | ||
|
|
||
| @case:discount-codes.the-code-works-in-any-case | ||
| Scenario: A customer types the code in lowercase | ||
| Given the site sells places at the Pottery for 10.00 | ||
| And the organiser has a promo code "SAVE10" taking 10 percent off | ||
| When a customer asks the price of a Pottery place with the code "save10" | ||
| Then the summary total is 9.00 | ||
|
|
||
| @rule:payments.a-wrong-code-quietly-buys-nothing | ||
| Rule: A wrong code quietly buys nothing | ||
| A code that matches nothing is simply ignored: no discount line appears | ||
| and the total stays at the full price. The site says nothing about it, | ||
| so a guessed code gives nothing away. | ||
|
|
||
| @case:discount-codes.a-wrong-code-changes-nothing | ||
| Scenario: A customer guesses at a code | ||
| Given the site sells places at the Pottery for 10.00 | ||
| And the organiser has a promo code "SAVE10" taking 10 percent off | ||
| When a customer asks the price of a Pottery place with the code "TENOFF" | ||
| Then the summary total is 10.00 | ||
| And the summary shows no discount line | ||
|
|
||
| @rule:payments.the-books-remember-what-a-code-was-used-for | ||
| @surface:webhook | ||
| Rule: The books remember what a code was used for | ||
| When a customer pays with a code, the site counts that use and writes | ||
| what the code was worth into the activity log, so the organiser can see | ||
| what their codes are costing them. | ||
|
|
||
| @case:discount-codes.a-paid-booking-counts-the-use | ||
| Scenario: A customer pays with the code | ||
| Given the site sells places at the Pottery for 10.00 | ||
| And the organiser has a promo code "SAVE10" taking 10 percent off | ||
| When a customer books a Pottery place with the code "SAVE10" and pays | ||
| Then the code "SAVE10" has been used once, worth 1.00 | ||
| And the activity log says the code "SAVE10" took 1.00 off |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| @story:servicing.telling-people-the-news | ||
| @owner:servicing @risk:low | ||
| @actor:organiser @actor:customer | ||
| @edition:managed @edition:self-hosted | ||
| @surface:admin | ||
| Feature: An owner tells people the news | ||
| The site keeps a news page. The owner writes a post in the admin, and a | ||
| visitor reads it at the post's own address. The News link only appears | ||
| once there is something to read, so nobody is led to an empty page. | ||
|
|
||
| @rule:servicing.a-post-is-read-at-its-own-address | ||
| @surface:public | ||
| Rule: A post is read at its own address | ||
| Creating a post is confirmed to the owner, and a visitor who was never | ||
| signed in reads its words — on the news page, and on the page the news | ||
| page links the post to. | ||
|
|
||
| @case:news.write-and-read-a-post | ||
| Scenario: The owner posts news and a visitor reads it | ||
| Given the public site is on | ||
| When the owner posts news called "Big launch" saying "Doors open Friday." | ||
| Then the owner is told the news post was created | ||
| And a visitor on the news page reads "Big launch" | ||
| And a visitor following "Big launch" from the news page reads "Doors open Friday." | ||
|
|
||
| @rule:servicing.the-news-link-waits-for-the-first-post | ||
| @surface:public | ||
| Rule: The news link waits for the first post | ||
| Before any post exists, the news page finds nothing and no public page | ||
| offers a News link. The first post brings both to life. | ||
|
|
||
| @case:news.no-posts-no-link | ||
| Scenario: There is no news yet | ||
| Given the public site is on | ||
| Then a visitor on the front page is offered no News link | ||
| And a visitor asking for the news page finds nothing there | ||
|
|
||
| @case:news.first-post-brings-the-link | ||
| Scenario: The first post brings the link | ||
| Given the public site is on | ||
| When the owner posts news called "We have opened" saying "Come and see us." | ||
| Then a visitor on the front page is offered a News link | ||
|
|
||
| @rule:servicing.taking-a-post-down-needs-its-exact-name | ||
| @surface:public | ||
| Rule: Taking a post down needs its exact name | ||
| Deleting a post asks the owner to type its name. A wrong name leaves the | ||
| post standing; the exact name removes it, and with the last post gone the | ||
| news page finds nothing again. | ||
|
|
||
| @case:news.wrong-name-keeps-the-post | ||
| Scenario: The owner types the wrong name | ||
| Given the public site is on | ||
| And the owner has posted news called "Spring fair" | ||
| When the owner tries to take down "Spring fair" typing "Spring fete" | ||
| Then the owner is told the post name does not match | ||
| And a visitor on the news page still reads "Spring fair" | ||
|
|
||
| @case:news.exact-name-removes-the-post | ||
| Scenario: The owner types the exact name | ||
| Given the public site is on | ||
| And the owner has posted news called "Spring fair" | ||
| When the owner takes down "Spring fair" typing its exact name | ||
| Then the owner is told the news post was deleted | ||
| And a visitor asking for the news page finds nothing there | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| And a visitor on the front page is offered no News link | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| // jscpd:ignore-start | ||
|
|
||
| import { Given, Then, When } from "@cucumber/cucumber"; | ||
| import { expect } from "@std/expect"; | ||
| import { ownerLastTold } from "#test/specs/support/buyer-questions.ts"; | ||
| import { | ||
| asMoney, | ||
| codeBoxOffered, | ||
| customerAsksPrice, | ||
| customerPaysWithCode, | ||
| expectDiscountLine, | ||
| organiserCreatesCode, | ||
| priceSummary, | ||
| summaryTotal, | ||
| } from "#test/specs/support/discount-codes.ts"; | ||
| import { sellPlacesAt } from "#test/specs/support/money.ts"; | ||
| import type { TicketsWorld } from "#test/specs/support/world.ts"; | ||
| import { activityMessages } from "#test-utils/activity-log.ts"; | ||
| import { | ||
| modifierUsageAmount, | ||
| modifierUsageCount, | ||
| } from "#test-utils/modifiers.ts"; | ||
|
|
||
| // jscpd:ignore-end | ||
|
|
||
| Given( | ||
| "the site sells places at the {word} for {float}", | ||
| async function (this: TicketsWorld, name: string, pounds: number) { | ||
| await sellPlacesAt(this, name, pounds.toFixed(2)); | ||
| }, | ||
| ); | ||
|
|
||
| When( | ||
| "the organiser creates a promo code {string} taking {int} percent off", | ||
| function (this: TicketsWorld, code: string, percent: number): Promise<void> { | ||
| return organiserCreatesCode(this, code, percent); | ||
| }, | ||
| ); | ||
|
|
||
| Given( | ||
| "the organiser has a promo code {string} taking {int} percent off", | ||
| function (this: TicketsWorld, code: string, percent: number): Promise<void> { | ||
| return organiserCreatesCode(this, code, percent); | ||
| }, | ||
| ); | ||
|
|
||
| Then( | ||
| "the organiser is told the modifier was created", | ||
| function (this: TicketsWorld): void { | ||
| expect(ownerLastTold(this)).toContain("Modifier created"); | ||
| }, | ||
| ); | ||
|
|
||
| /** Both box steps ask the same question of the served page; only the answer | ||
| * they insist on differs. */ | ||
| const boxExpected = (offered: boolean) => | ||
| async function (this: TicketsWorld, listing: string): Promise<void> { | ||
| expect(await codeBoxOffered(this, listing)).toBe(offered); | ||
| }; | ||
|
|
||
| Then("the {word} booking page offers no promo code box", boxExpected(false)); | ||
|
|
||
| Then("the {word} booking page offers a promo code box", boxExpected(true)); | ||
|
|
||
| /** A step that hands the listing and the typed code straight to one journey. */ | ||
| const codeJourney = ( | ||
| journey: ( | ||
| world: TicketsWorld, | ||
| listing: string, | ||
| code: string, | ||
| ) => Promise<void>, | ||
| ) => | ||
| function (this: TicketsWorld, listing: string, code: string): Promise<void> { | ||
| return journey(this, listing, code); | ||
| }; | ||
|
|
||
| When( | ||
| "a customer asks the price of a {word} place with the code {string}", | ||
| codeJourney(customerAsksPrice), | ||
| ); | ||
|
|
||
| Then( | ||
| "the summary shows the place at {float}", | ||
| function (this: TicketsWorld, pounds: number): void { | ||
| expect(priceSummary(this)).toContain(asMoney(pounds.toFixed(2))); | ||
| }, | ||
| ); | ||
|
|
||
| Then( | ||
| "the summary shows the discount line {string} taking off {float}", | ||
| function (this: TicketsWorld, code: string, pounds: number): void { | ||
| expectDiscountLine(this, code, pounds.toFixed(2)); | ||
| }, | ||
| ); | ||
|
|
||
| Then( | ||
| "the summary total is {float}", | ||
| function (this: TicketsWorld, pounds: number): void { | ||
| expect(summaryTotal(this)).toContain(asMoney(pounds.toFixed(2))); | ||
| }, | ||
| ); | ||
|
|
||
| Then("the summary shows no discount line", function (this: TicketsWorld): void { | ||
| // No negative figure anywhere: nothing was taken off at all. | ||
| expect(priceSummary(this)).not.toContain("-£"); | ||
|
stefan-burke marked this conversation as resolved.
Resolved
|
||
| }); | ||
|
|
||
| When( | ||
| "a customer books a {word} place with the code {string} and pays", | ||
| codeJourney(customerPaysWithCode), | ||
| ); | ||
|
|
||
| Then( | ||
| "the code {string} has been used once, worth {float}", | ||
| async function (this: TicketsWorld, code: string, pounds: number) { | ||
| const modifierId = this.things.require("record", code); | ||
| expect(await modifierUsageCount(modifierId)).toBe(1); | ||
| expect(await modifierUsageAmount(modifierId)).toBe( | ||
| Math.round(pounds * 100), | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| Then( | ||
| "the activity log says the code {string} took {float} off", | ||
| async function (this: TicketsWorld, code: string, pounds: number) { | ||
| expect(await activityMessages()).toContain( | ||
| `Promo code '${code}' used: ${asMoney(pounds.toFixed(2))} off`, | ||
| ); | ||
| }, | ||
| ); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.