Skip to content
wannacode

Promo codes: from the request to the release

In short: One request, "I want promo codes for the autumn", goes through everything in the course. The questions find the need and the people nobody asks. The story is split by its rules, the validity period goes through Example Mapping. The code rules become scenarios against a system with bugs, and since the change touches the total, delivery is protected from regression too. The caught bug becomes a report, two scenarios become code, and the matrix says what is touched when the requirement changes.

Problem

The request

The shop owner writes on a Monday morning:

"I want promo codes for the autumn. 10 % off, until the end of November. We release in two weeks."

Three sentences. They hold at least five rules nobody has stated yet, one open topic with money in it, and one person who will find out about the change only when they cannot do their job. In this project you go along the whole path: from the three sentences to the released and checked change.

The idea

The path

Every step comes from a module of the course:

  1. the questions: the need behind the request and the people who have to be asked;
  2. the story and the split: the request is big; it is cut by its rules;
  3. Example Mapping: the rules, the examples and the questions about the validity period of the code;
  4. the scenarios and the tests: the code rules become scenarios; the change also touches the total, and delivery depends on it, so it is protected from regression;
  5. the report: the bug the change brought in, described so that it gets fixed the first time;
  6. the automation: two scenarios for the promo code become code in the four languages;
  7. the matrix and the change: what the tests cover and what is touched when the requirement changes.
More precisely, if you are curious

In the browser, the scenarios run against a system with bugs. For the promo code it knows three of the rules: 10%, products on sale and which discount applies with loyalty. The validity period and "The code has already been used" are not in it: they are in the map, in the matrix and in the automation, where the scenarios run with the real tools. The regression, the table and the report are about delivery: the code change alters the total that delivery depends on, and delivery has to stay correct.

Try it

The questions

1. What is the first question to the owner?
2. Who else do you have to ask, apart from the owner?
3. Is the threshold for free delivery counted before the discount or after it? The owner says: "I have not thought about it."
4. Which question is leading?

The idea

The answers

After talking to the owner, marketing and accounting, the request looks like this:

  • the need: more new customers in the autumn. Metric: first orders from new customers per month, compared with last autumn; measured at the end of November;
  • the rules: the code gives 10 % off; it is not valid on discounted products; it is used in one paid order ("The code has already been used"); it is valid until its last day inclusive ("The code has expired"); it does not add up with loyalty, the bigger one applies;
  • accounting: wants to see which code gave each discount;
  • open question: is the 50 € threshold for free delivery counted before the discount or after it. The owner decides; until then nobody else does.

Five rules in one story are a lot. The next step is the split.

Exercise

The story and the map

1. The split. The story with all the rules of the code is too big for one iteration.

Choose the pattern the story is cut by, then build the split in the order it is done.

Story

As a customer I want to enter a promo code and get a discount so that I buy for less.

Step 1: Which pattern splits it?

2. The validity period. The first piece after 10 % is the validity period. The three amigos sat down with the cards.

Put each card in its place, then decide what the story is.

Story

As a customer I want to know until when the code is valid so that I use it in time.

For every card press “Place as…” and choose: a rule (it becomes a column), an example for a rule, or a question. The arrow keys move between the choices, Esc closes. You can also use the mouse: drag the card to its place.

Rules

No rules yet. Place a card as a rule and it becomes a column.

Questions

  • No questions yet.

Cards not placed yet

  • An expired code is refused with "The code has expired", and the total stays without the discount

  • An order on 30 November with AUTUMN10: a T-shirt for 20.00 € costs 18.00 €

  • The code was entered on 30 November at 23:50, and the order was paid on 1 December at 00:10. Is it valid?

  • The last day ends at midnight by whose clock: the shop's or the customer's?

  • The code is valid up to and including its last day

  • An order on 1 December with AUTUMN10: "The code has expired", the T-shirt is still 20.00 €

  • AUTUMN10 is valid until 30 November. An order on 15 November: a T-shirt for 20.00 € costs 18.00 €

Exercise

The scenarios for the code

Three of the rules from the split run right here: 10%, products on sale and the loyal customer with a code. One scenario per example, in the owner's language.

Write the scenarios for the promo code so that all six bugs around the discounts are caught.

GherkinDiscounts

The idea

The change touches delivery

The promo code changes the cart total. Delivery depends on the total: free from 50.00 € inclusive, otherwise 4.90 €. The programmer of the code will touch exactly the calculation of the total.

Whatever the owner decides about the threshold before or after the discount, the delivery rule from the total has to stay correct. That is why, before the change, delivery is protected from regression: scenarios for the agreed examples and tests for the technical cases, which fail if something around the boundary breaks.

Exercise

The regression of delivery

1. The scenarios. Few scenarios, one for each agreed example.

Write the delivery scenarios so that they catch the five bugs around the boundary and the empty cart.

GherkinFree delivery

2. The technical cases. The table is for the component-level test: the cases the business does not want to read.

Write the cases that catch all six bugs, including the leftover of the old tariff for big orders.

The rule: Delivery is free for an order from 50 € inclusive, otherwise it is 4.90 €. An empty cart cannot be ordered.

InputExpectedThe correct system
#Cart totalRefusedDelivery

An empty result cell is not checked. Amounts are written like 49.99.

Exercise

The caught bug

1. The report. The programmer of the promo code calculates the total with fractional numbers and rounds it to whole euros. The scenario for 49.99 € fails.

Find a total where the bug shows, and write the report.

The bug you caught: Free delivery

for amounts with cents just under 50 €

The rule: Delivery is free for an order from 50 € inclusive, otherwise it is 4.90 €. An empty cart cannot be ordered.

Steps to reproduce

  1. No steps yet. Add them from the vocabulary below.

The steps come from the vocabulary of the feature, as in the scenarios. The values go into the step itself: an amount like 50.00.

Expected (what should happen by the rule)
Actual (what happens in the system with the bug)
Severity (how much harm the defect does)
Priority (how soon to fix it)

The idea

Automating two scenarios

The two promo code scenarios the owner read and accepted:

promo.feature
# language: en
Feature: Promo code
A promo code gives 10 % off up to and including its last day.
Scenario: The code gives 10 % off
Given a cart of 20.00 €
When I apply the code AUTUMN10
Then the total is 18.00 €
Scenario: An expired code
Given a cart of 20.00 €
And the code AUTUMN10 has expired
When I apply the code AUTUMN10
Then I am told the code has expired
And the total is 20.00 €

The class that applies the code. The total is in cents, and the day is an input, not "today": this is the answer to the midnight question from the map and to the lesson about tests in CI:

src/main/java/shop/PromoCode.java
package shop;
import java.time.LocalDate;
public class PromoCode {
private final String code;
private final LocalDate lastDay;
public PromoCode(String code, LocalDate lastDay) {
this.code = code;
this.lastDay = lastDay;
}
// 10 % off the total in cents. An expired code is refused.
public int apply(int total, LocalDate day) {
if (day.isAfter(lastDay)) {
throw new IllegalArgumentException("The code has expired");
}
return total - total / 10;
}
}

The "When" step applies the code, and the "Then" step checks the message:

src/test/java/shop/PromoSteps.java
@When("I apply the code {word}")
public void apply(String code) {
try {
total = new PromoCode(code, LAST_DAY).apply(total, today);
} catch (IllegalArgumentException e) {
refusal = e.getMessage();
}
}
src/test/java/shop/PromoSteps.java
@Then("I am told the code has expired")
public void toldExpired() {
assertEquals("The code has expired", refusal);
}

The whole steps file, run with the real tool. Both scenarios pass:

src/test/java/shop/PromoSteps.java
package shop;
import io.cucumber.java.ParameterType;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import java.time.LocalDate;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class PromoSteps {
// The last day of the code in the scenarios.
private static final LocalDate LAST_DAY = LocalDate.of(2026, 11, 30);
private int total;
private LocalDate today = LocalDate.of(2026, 11, 15);
private String refusal;
@ParameterType(name = "amount", value = "\\d+\\.\\d{2}")
public int amount(String text) {
return Integer.parseInt(text.replace(".", ""));
}
@Given("a cart of {amount} €")
public void cart(int cents) {
total = cents;
}
@Given("the code {word} has expired")
public void expired(String code) {
today = LAST_DAY.plusDays(1);
}
@When("I apply the code {word}")
public void apply(String code) {
try {
total = new PromoCode(code, LAST_DAY).apply(total, today);
} catch (IllegalArgumentException e) {
refusal = e.getMessage();
}
}
@Then("the total is {amount} €")
public void totalIs(int cents) {
assertEquals(cents, total);
}
@Then("I am told the code has expired")
public void toldExpired() {
assertEquals("The code has expired", refusal);
}
}
Output
PASS An expired code
PASS The code gives 10 % off
Scenarios: 2, passed: 2, failed: 0

And if the programmer forgets the validity period, the expired code still gives 10 % off, and the "An expired code" scenario fails:

src/main/java/shop/PromoCode.java
package shop;
import java.time.LocalDate;
public class PromoCode {
private final String code;
private final LocalDate lastDay;
public PromoCode(String code, LocalDate lastDay) {
this.code = code;
this.lastDay = lastDay;
}
// 10 % off the total in cents. An expired code is refused.
public int apply(int total, LocalDate day) {
return total - total / 10;
}
}
Output
FAIL An expired code: expected: <The code has expired> but was: <null>
PASS The code gives 10 % off
Scenarios: 2, passed: 1, failed: 1
More precisely, if you are curious

These scenarios are not in the vocabulary of the course engine, so they are not checked against a system with a bug in the browser. They are run with Cucumber-JVM, Reqnroll, behave and cucumber-js, as in the "Automation" module, and their output is verified. The discount is rounded down to a whole cent (total / 10 in integers); for totals like 20.00 € that makes no difference, but it is a decision accounting has to confirm.

Exercise

The matrix and the change

1. Coverage and impact. The autumn is over. The metric showed that most codes were used by existing customers, and the owner changes the rule: the code is valid only for the customer's first order.

Mark the links. Then, in the second step, choose which tests have to be reviewed for the change.

Tests

  • T-1: A new customer, a T-shirt for 20.00 € with AUTUMN10: 18.00 €.
  • T-2: A new customer, discounted jeans for 40.00 € with AUTUMN10: 40.00 €.
  • T-3: A new customer, a T-shirt for 20.00 € and discounted jeans for 40.00 € with AUTUMN10: 58.00 €.
  • T-4: The customer has already paid an order with AUTUMN10. In a new order they enter AUTUMN10: "The code has already been used".
  • T-5: A new customer enters AUTUMN10 on 1 December, after the last day: "The code has expired".
  • T-6: A loyal customer with AUTUMN10, a T-shirt for 20.00 €: 18.00 €, not 17.00 €.
  • T-7: A new customer enters AUTUMN10 on 30 November, the last day: a T-shirt for 20.00 € costs 18.00 €.

Press the cell where the test checks the requirement; press again to remove the link. The arrow keys move between the cells, Space or Enter marks.

RequirementsT-1: A new customer, a T-shirt for 20.00 € with AUTUMN10: 18.00 €.T-2: A new customer, discounted jeans for 40.00 € with AUTUMN10: 40.00 €.T-3: A new customer, a T-shirt for 20.00 € and discounted jeans for 40.00 € with AUTUMN10: 58.00 €.T-4: The customer has already paid an order with AUTUMN10. In a new order they enter AUTUMN10: "The code has already been used".T-5: A new customer enters AUTUMN10 on 1 December, after the last day: "The code has expired".T-6: A loyal customer with AUTUMN10, a T-shirt for 20.00 €: 18.00 €, not 17.00 €.T-7: A new customer enters AUTUMN10 on 30 November, the last day: a T-shirt for 20.00 € costs 18.00 €.Covered by
R-1 A promo code gives 10 % off.0
R-2 A promo code does not apply to discounted products.0
R-3 A code can be used in one paid order. A second try: "The code has already been used".0
R-4 A code is valid up to and including its last day. After it: "The code has expired".0
R-5 Discounts do not add up: the larger one applies.0
R-6 The order shows which code gave the discount.0
Checks0000000

Test

Check yourself

1. The request is about the promo code. Why does the project also have scenarios and tests for delivery?
2. What makes the matrix in this project useful when the requirement changes?
3. Which of these is "done" for the promo codes?
4. The question "whose time does the last day end by" from the map turned out to matter in the code too. Where?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·