Archived
An office box is a total, not an assortment — and every size says who it feeds
I had built in an assumption nobody stated, and a ticked list made it a claim: that a $24 box contains
mini muffins AND mini scones AND mini cinnamon rolls. It doesn't. It is twelve items, mixed in sixes,
from those three. The page was describing the offer wrongly — worse than the spreadsheet, which at least
didn't say either way.
The fix keeps one rule for the whole page rather than a mode for this table: a line is something you get,
and a choice within a line is written into its name. The parties and wedding tables already read that way
("Cupcakes or sugar cookies"), so the office table becomes one line — "Any mix of mini muffins, mini
scones and mini cinnamon rolls" — with the size's item count as its cell. Three ticks became one, and the
"6+6" detail moved to the note where the bakery's own minimum already lives: "Baked in sixes, so a dozen
can be six muffins and six scones."
Confirmed, not guessed, for the other two: parties are a cake AND a dozen to hand round; a wedding
package is everything listed. Their ticked lists were already right.
SERVES. "Small" answers nothing for someone ordering for an office, which is the whole question. Columns
gain an optional `serves` — schema, view, a box in the table editor, a line under the price. The office
numbers are estimates from the item counts and are meant to be corrected by the people who pack the
boxes. The party and wedding columns are already named by head count, so theirs stay empty rather than
saying it twice.
53 tests: the office table is one mixed line, its columns say who they feed, and the head-count tables
don't repeat themselves. The blank-cell test builds its own table now, since no seeded line is blank
across any more.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -78,6 +78,7 @@ public class AdminCateringController {
|
|||||||
private Long id;
|
private Long id;
|
||||||
private String label = "";
|
private String label = "";
|
||||||
private String price = "";
|
private String price = "";
|
||||||
|
private String serves = "";
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
public void setId(Long id) { this.id = id; }
|
public void setId(Long id) { this.id = id; }
|
||||||
@@ -87,6 +88,9 @@ public class AdminCateringController {
|
|||||||
|
|
||||||
public String getPrice() { return price; }
|
public String getPrice() { return price; }
|
||||||
public void setPrice(String price) { this.price = price; }
|
public void setPrice(String price) { this.price = price; }
|
||||||
|
|
||||||
|
public String getServes() { return serves; }
|
||||||
|
public void setServes(String serves) { this.serves = serves; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class LineForm {
|
public static class LineForm {
|
||||||
@@ -172,7 +176,7 @@ public class AdminCateringController {
|
|||||||
form.getName(),
|
form.getName(),
|
||||||
form.getBlurb(),
|
form.getBlurb(),
|
||||||
form.getColumns().stream()
|
form.getColumns().stream()
|
||||||
.map(c -> new CateringMenu.TierEdit(c.getId(), c.getLabel(), c.getPrice()))
|
.map(c -> new CateringMenu.TierEdit(c.getId(), c.getLabel(), c.getPrice(), c.getServes()))
|
||||||
.toList(),
|
.toList(),
|
||||||
form.getLines().stream()
|
form.getLines().stream()
|
||||||
.map(l -> new CateringMenu.RowEdit(l.getId(), l.getLabel(), l.getValues()))
|
.map(l -> new CateringMenu.RowEdit(l.getId(), l.getLabel(), l.getValues()))
|
||||||
@@ -302,6 +306,7 @@ public class AdminCateringController {
|
|||||||
// The price comes back written out ("$24") and goes out again as whatever is left in the box;
|
// The price comes back written out ("$24") and goes out again as whatever is left in the box;
|
||||||
// Money reads either.
|
// Money reads either.
|
||||||
column.setPrice(tier.price() == null ? "" : tier.price());
|
column.setPrice(tier.price() == null ? "" : tier.price());
|
||||||
|
column.setServes(tier.serves() == null ? "" : tier.serves());
|
||||||
return column;
|
return column;
|
||||||
}).collect(Collectors.toCollection(ArrayList::new)));
|
}).collect(Collectors.toCollection(ArrayList::new)));
|
||||||
form.setLines(table.rows().stream().map(row -> {
|
form.setLines(table.rows().stream().map(row -> {
|
||||||
|
|||||||
@@ -34,8 +34,11 @@ public class CateringMenu {
|
|||||||
this.notes = notes;
|
this.notes = notes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A column. {@code price} is ready to print ("$24"), and null when the column doesn't state one. */
|
/**
|
||||||
public record TierView(Long id, String label, String price) {}
|
* A column. {@code price} is ready to print ("$24") and null when the column doesn't state one;
|
||||||
|
* {@code serves} is roughly how many people it feeds, and null when the label already says.
|
||||||
|
*/
|
||||||
|
public record TierView(Long id, String label, String price, String serves) {}
|
||||||
|
|
||||||
/** A line, with one entry per column, in column order. */
|
/** A line, with one entry per column, in column order. */
|
||||||
public record RowView(Long id, String label, List<String> values) {}
|
public record RowView(Long id, String label, List<String> values) {}
|
||||||
@@ -48,7 +51,7 @@ public class CateringMenu {
|
|||||||
|
|
||||||
// What the admin screen sends back. A whole table at a time — see CateringPackage#arrange.
|
// What the admin screen sends back. A whole table at a time — see CateringPackage#arrange.
|
||||||
// `price` is the raw text from the box ("24", "$24.50", ""); Money decides what it means.
|
// `price` is the raw text from the box ("24", "$24.50", ""); Money decides what it means.
|
||||||
public record TierEdit(Long id, String label, String price) {}
|
public record TierEdit(Long id, String label, String price, String serves) {}
|
||||||
|
|
||||||
public record RowEdit(Long id, String label, List<String> values) {}
|
public record RowEdit(Long id, String label, List<String> values) {}
|
||||||
|
|
||||||
@@ -99,7 +102,7 @@ public class CateringMenu {
|
|||||||
table.replaceNotes(clean(edit.notes()));
|
table.replaceNotes(clean(edit.notes()));
|
||||||
table.arrange(
|
table.arrange(
|
||||||
orEmpty(edit.tiers()).stream()
|
orEmpty(edit.tiers()).stream()
|
||||||
.map(t -> new CateringPackage.Heading(t.id(), t.label(), t.price()))
|
.map(t -> new CateringPackage.Heading(t.id(), t.label(), t.price(), t.serves()))
|
||||||
.toList(),
|
.toList(),
|
||||||
orEmpty(edit.rows()).stream()
|
orEmpty(edit.rows()).stream()
|
||||||
.map(r -> new CateringPackage.Line(r.id(), r.label(), orEmpty(r.values())))
|
.map(r -> new CateringPackage.Line(r.id(), r.label(), orEmpty(r.values())))
|
||||||
@@ -171,7 +174,7 @@ public class CateringMenu {
|
|||||||
|
|
||||||
private static PackageView toView(CateringPackage table) {
|
private static PackageView toView(CateringPackage table) {
|
||||||
List<TierView> tiers = table.getTiers().stream()
|
List<TierView> tiers = table.getTiers().stream()
|
||||||
.map(t -> new TierView(t.getId(), t.getLabel(), t.getPrice()))
|
.map(t -> new TierView(t.getId(), t.getLabel(), t.getPrice(), t.getServes()))
|
||||||
.toList();
|
.toList();
|
||||||
List<RowView> rows = table.getRows().stream()
|
List<RowView> rows = table.getRows().stream()
|
||||||
.map(r -> new RowView(r.getId(), r.getLabel(), r.getValues()))
|
.map(r -> new RowView(r.getId(), r.getLabel(), r.getValues()))
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ public class CateringPackage extends BaseEntity {
|
|||||||
* A column as the editor left it — its heading and its price. A null {@code id} is one they just
|
* A column as the editor left it — its heading and its price. A null {@code id} is one they just
|
||||||
* added. (Named for the heading rather than the column so as not to shadow {@code @Column}.)
|
* added. (Named for the heading rather than the column so as not to shadow {@code @Column}.)
|
||||||
*/
|
*/
|
||||||
public record Heading(Long id, String label, String price) {}
|
public record Heading(Long id, String label, String price, String serves) {}
|
||||||
|
|
||||||
/** A line as the editor left it, with one value per column — blanks included. */
|
/** A line as the editor left it, with one value per column — blanks included. */
|
||||||
public record Line(Long id, String label, List<String> values) {}
|
public record Line(Long id, String label, List<String> values) {}
|
||||||
@@ -136,7 +136,7 @@ public class CateringPackage extends BaseEntity {
|
|||||||
int columnNumber = 1;
|
int columnNumber = 1;
|
||||||
for (Heading column : columns) {
|
for (Heading column : columns) {
|
||||||
CateringTier tier = column.id() == null ? new CateringTier(this) : tier(column.id());
|
CateringTier tier = column.id() == null ? new CateringTier(this) : tier(column.id());
|
||||||
tier.describe(column.label(), column.price());
|
tier.describe(column.label(), column.price(), column.serves());
|
||||||
tier.moveTo(columnNumber++);
|
tier.moveTo(columnNumber++);
|
||||||
arrangedTiers.add(tier);
|
arrangedTiers.add(tier);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ public class CateringTier extends BaseEntity {
|
|||||||
@Column(name = "price_cents")
|
@Column(name = "price_cents")
|
||||||
private Integer priceCents;
|
private Integer priceCents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roughly how many people this column feeds — "About 6–8 people".
|
||||||
|
*
|
||||||
|
* <p>Free text, and optional. A column named "Small" says nothing about how many it feeds, which is
|
||||||
|
* the question anyone ordering for an office actually has; a column named "125 people" has already
|
||||||
|
* answered it and leaves this blank rather than saying it twice.
|
||||||
|
*/
|
||||||
|
@Column(length = 80)
|
||||||
|
private String serves;
|
||||||
|
|
||||||
@Column(name = "position", nullable = false)
|
@Column(name = "position", nullable = false)
|
||||||
private int position;
|
private int position;
|
||||||
|
|
||||||
@@ -42,11 +52,16 @@ public class CateringTier extends BaseEntity {
|
|||||||
this.cateringPackage = cateringPackage;
|
this.cateringPackage = cateringPackage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param price as the editor typed it; empty for a column that doesn't state one */
|
/**
|
||||||
void describe(String label, String price) {
|
* @param price as the editor typed it; empty for a column that doesn't state one
|
||||||
|
* @param serves roughly how many people, or empty
|
||||||
|
*/
|
||||||
|
void describe(String label, String price, String serves) {
|
||||||
this.label = Text.required(label, 120,
|
this.label = Text.required(label, 120,
|
||||||
"Every column needs a heading — a size like \"Large\", or who it feeds like \"15–20 people\".");
|
"Every column needs a heading — a size like \"Large\", or who it feeds like \"15–20 people\".");
|
||||||
this.priceCents = Money.cents(price);
|
this.priceCents = Money.cents(price);
|
||||||
|
String trimmed = Text.optional(serves, 80);
|
||||||
|
this.serves = trimmed.isEmpty() ? null : trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void moveTo(int position) {
|
void moveTo(int position) {
|
||||||
@@ -66,6 +81,10 @@ public class CateringTier extends BaseEntity {
|
|||||||
return Money.format(priceCents);
|
return Money.format(priceCents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getServes() {
|
||||||
|
return serves;
|
||||||
|
}
|
||||||
|
|
||||||
public int getPosition() {
|
public int getPosition() {
|
||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
-- Two things the spreadsheet never said out loud, and the page was getting wrong.
|
||||||
|
--
|
||||||
|
-- 1. AN OFFICE BOX IS A TOTAL, NOT AN ASSORTMENT. A Small box is twelve items, mixed in sixes from the
|
||||||
|
-- minis — not twelve muffins AND twelve scones AND some cinnamon rolls, which is what three ticked
|
||||||
|
-- lines on a card claimed. The bakery confirmed it: mix them, in sixes, to the size's item count.
|
||||||
|
--
|
||||||
|
-- The fix keeps the shape everything else uses rather than inventing a mode for this one table: the
|
||||||
|
-- alternative goes INTO the line, the way "Cupcakes or sugar cookies" already does in the parties and
|
||||||
|
-- wedding tables. One line, "Any mix of…", and the size's cell carries the count. So the rule for the
|
||||||
|
-- whole page is now sayable in one sentence: lines are things you get (and), and a choice within a
|
||||||
|
-- line is written into its name (or).
|
||||||
|
--
|
||||||
|
-- 2. "SMALL" DOESN'T TELL ANYONE HOW MANY PEOPLE IT FEEDS. The party and wedding tables size themselves
|
||||||
|
-- by head count; the office table sizes itself by box. `serves` gives every column somewhere to say
|
||||||
|
-- it, and the admin gets a box for it. The office numbers below are estimates from the item counts and
|
||||||
|
-- are meant to be corrected by the people who pack the boxes.
|
||||||
|
|
||||||
|
alter table catering_tier add column serves varchar(80);
|
||||||
|
|
||||||
|
-- --- 1. the office box's contents ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
update catering_row r
|
||||||
|
set label = 'Any mix of mini muffins, mini scones and mini cinnamon rolls'
|
||||||
|
from catering_package p
|
||||||
|
where p.id = r.package_id
|
||||||
|
and p.name = 'Office boxes'
|
||||||
|
and r.label = 'Mini muffins';
|
||||||
|
|
||||||
|
update catering_row_value v set value = '12 items'
|
||||||
|
from catering_row r
|
||||||
|
where r.id = v.row_id and r.label like 'Any mix of%' and v.position = 0 and v.value = 'A dozen';
|
||||||
|
update catering_row_value v set value = '18 items'
|
||||||
|
from catering_row r
|
||||||
|
where r.id = v.row_id and r.label like 'Any mix of%' and v.position = 1 and v.value = 'Eighteen';
|
||||||
|
update catering_row_value v set value = '24 items'
|
||||||
|
from catering_row r
|
||||||
|
where r.id = v.row_id and r.label like 'Any mix of%' and v.position = 2 and v.value = 'Two dozen';
|
||||||
|
|
||||||
|
-- The other two lines are now named inside the first one. Guarded on the wording V6 left, so a line the
|
||||||
|
-- bakery has since edited stays and can be tidied by hand.
|
||||||
|
delete from catering_row r
|
||||||
|
using catering_package p
|
||||||
|
where p.id = r.package_id
|
||||||
|
and p.name = 'Office boxes'
|
||||||
|
and r.label in ('Mini scones', 'Mini cinnamon rolls');
|
||||||
|
|
||||||
|
-- "6+6" was the spreadsheet showing how a dozen scones splits by flavour. With one mixed line, that
|
||||||
|
-- belongs in the note, where the bakery's own minimum already lives.
|
||||||
|
update catering_package_note
|
||||||
|
set body = 'Baked in sixes, so a dozen can be six muffins and six scones.'
|
||||||
|
where body = 'Everything is baked in sixes, so each item comes in multiples of six.';
|
||||||
|
|
||||||
|
-- --- 2. how many people each column feeds -------------------------------------------------------------
|
||||||
|
|
||||||
|
update catering_tier t set serves = 'About 6–8 people'
|
||||||
|
from catering_package p
|
||||||
|
where p.id = t.package_id and p.name = 'Office boxes' and t.label = 'Small' and t.serves is null;
|
||||||
|
update catering_tier t set serves = 'About 10–12 people'
|
||||||
|
from catering_package p
|
||||||
|
where p.id = t.package_id and p.name = 'Office boxes' and t.label = 'Medium' and t.serves is null;
|
||||||
|
update catering_tier t set serves = 'About 15–20 people'
|
||||||
|
from catering_package p
|
||||||
|
where p.id = t.package_id and p.name = 'Office boxes' and t.label = 'Large' and t.serves is null;
|
||||||
|
|
||||||
|
-- The party and wedding columns are already named by head count ("15–20 people", "125 people"), so a
|
||||||
|
-- `serves` line under the price would say it twice. Left null on purpose.
|
||||||
@@ -47,6 +47,9 @@
|
|||||||
th:aria-label="|Heading for column ${c.count}|">
|
th:aria-label="|Heading for column ${c.count}|">
|
||||||
<input class="field mt-1" th:field="*{columns[__${c.index}__].price}"
|
<input class="field mt-1" th:field="*{columns[__${c.index}__].price}"
|
||||||
placeholder="$24 — leave empty to ask" th:aria-label="|Price for column ${c.count}|">
|
placeholder="$24 — leave empty to ask" th:aria-label="|Price for column ${c.count}|">
|
||||||
|
<!--/* Optional: only worth filling in when the heading isn't already a head count. */-->
|
||||||
|
<input class="field mt-1" th:field="*{columns[__${c.index}__].serves}"
|
||||||
|
placeholder="Serves — optional" th:aria-label="|How many people column ${c.count} feeds|">
|
||||||
<div class="mt-1 flex justify-center gap-1">
|
<div class="mt-1 flex justify-center gap-1">
|
||||||
<button type="submit" name="do" th:value="|move-column:${c.index}:-1|" class="btn-icon"
|
<button type="submit" name="do" th:value="|move-column:${c.index}:-1|" class="btn-icon"
|
||||||
th:disabled="${c.first}" aria-label="Move this column left">←</button>
|
th:disabled="${c.first}" aria-label="Move this column left">←</button>
|
||||||
|
|||||||
@@ -73,6 +73,9 @@
|
|||||||
<p th:if="${tier.price}" class="mt-2 font-adbhashitha text-4xl text-bakery-900" th:text="${tier.price}">$24</p>
|
<p th:if="${tier.price}" class="mt-2 font-adbhashitha text-4xl text-bakery-900" th:text="${tier.price}">$24</p>
|
||||||
<!--/* No price means "ask us" — say so rather than leaving a hole in the card. */-->
|
<!--/* No price means "ask us" — say so rather than leaving a hole in the card. */-->
|
||||||
<p th:unless="${tier.price}" class="mt-2 font-adbhashitha text-2xl text-bakery-700">Ask us</p>
|
<p th:unless="${tier.price}" class="mt-2 font-adbhashitha text-2xl text-bakery-700">Ask us</p>
|
||||||
|
<!--/* "Small" doesn't tell anyone how many it feeds. A column whose heading is already a
|
||||||
|
head count leaves this blank rather than saying it twice. */-->
|
||||||
|
<p th:if="${tier.serves}" class="mt-1 text-sm text-bakery-600" th:text="${tier.serves}">About 6–8 people</p>
|
||||||
|
|
||||||
<ul class="mt-6 space-y-3 text-bakery-800">
|
<ul class="mt-6 space-y-3 text-bakery-800">
|
||||||
<li th:each="row : ${table.rows}" class="flex gap-3">
|
<li th:each="row : ${table.rows}" class="flex gap-3">
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ class AdminPagesTest {
|
|||||||
// Indexed names are what let Spring bind the grid back into the right cells.
|
// Indexed names are what let Spring bind the grid back into the right cells.
|
||||||
.andExpect(content().string(containsString("name=\"columns[0].label\"")))
|
.andExpect(content().string(containsString("name=\"columns[0].label\"")))
|
||||||
.andExpect(content().string(containsString("name=\"lines[0].values[1]\"")))
|
.andExpect(content().string(containsString("name=\"lines[0].values[1]\"")))
|
||||||
.andExpect(content().string(containsString("value=\"Eighteen\"")))
|
.andExpect(content().string(containsString("value=\"18 items\"")))
|
||||||
// The price round-trips as text: it came out "$24" and goes back the same way.
|
// The price round-trips as text: it came out "$24" and goes back the same way.
|
||||||
.andExpect(content().string(containsString("value=\"$24\"")));
|
.andExpect(content().string(containsString("value=\"$24\"")));
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,7 @@ class AdminPagesTest {
|
|||||||
|
|
||||||
// And nothing was written: the live page still says what it said.
|
// And nothing was written: the live page still says what it said.
|
||||||
mvc.perform(get("/api/catering"))
|
mvc.perform(get("/api/catering"))
|
||||||
.andExpect(content().string(containsString("A dozen")));
|
.andExpect(content().string(containsString("12 items")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -69,12 +69,18 @@ class CateringMenuTest {
|
|||||||
.containsExactly("Small", "Medium", "Large");
|
.containsExactly("Small", "Medium", "Large");
|
||||||
assertThat(office.tiers()).extracting(CateringMenu.TierView::price)
|
assertThat(office.tiers()).extracting(CateringMenu.TierView::price)
|
||||||
.containsExactly("$24", "$32", "$40");
|
.containsExactly("$24", "$32", "$40");
|
||||||
|
// One line, not three: a Small box is twelve items mixed in sixes, not twelve of each thing. The
|
||||||
|
// choice is written into the line's name, which is the same rule the other two tables follow.
|
||||||
assertThat(office.rows()).extracting(CateringMenu.RowView::label)
|
assertThat(office.rows()).extracting(CateringMenu.RowView::label)
|
||||||
.containsExactly("Mini muffins", "Mini scones", "Mini cinnamon rolls");
|
.containsExactly("Any mix of mini muffins, mini scones and mini cinnamon rolls");
|
||||||
assertThat(office.rows().get(0).values()).containsExactly("A dozen", "Eighteen", "Two dozen");
|
assertThat(office.rows().get(0).values()).containsExactly("12 items", "18 items", "24 items");
|
||||||
assertThat(office.rows().get(1).values().get(2)).isEqualTo("Two dozen, in up to four flavors");
|
// "Small" says nothing about how many it feeds; this does.
|
||||||
|
assertThat(office.tiers()).extracting(CateringMenu.TierView::serves)
|
||||||
|
.containsExactly("About 6–8 people", "About 10–12 people", "About 15–20 people");
|
||||||
|
// The party and wedding columns are named by head count already, so they don't repeat it.
|
||||||
|
assertThat(tables.get(1).tiers()).extracting(CateringMenu.TierView::serves).containsOnlyNulls();
|
||||||
assertThat(office.blurb()).contains("morning meeting");
|
assertThat(office.blurb()).contains("morning meeting");
|
||||||
assertThat(office.notes()).anySatisfy(note -> assertThat(note).contains("baked in sixes"));
|
assertThat(office.notes()).anySatisfy(note -> assertThat(note).contains("Baked in sixes"));
|
||||||
|
|
||||||
assertThat(tables.get(1).tiers()).extracting(CateringMenu.TierView::label)
|
assertThat(tables.get(1).tiers()).extracting(CateringMenu.TierView::label)
|
||||||
.containsExactly("15–20 people", "20–30 people", "30–40 people");
|
.containsExactly("15–20 people", "20–30 people", "30–40 people");
|
||||||
@@ -129,11 +135,17 @@ class CateringMenuTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void keepsBlankCellsRatherThanCollapsingThem() {
|
void keepsBlankCellsRatherThanCollapsingThem() {
|
||||||
// "Mini cinnamon rolls" is named but not quantified in the spreadsheet. Dropping its blanks
|
// No seeded line is blank across any more, so this uses a table of its own making: a blank cell
|
||||||
// would shorten the line and shift everything after it.
|
// still has to survive a save, because dropping one would shorten the line and shift the rest.
|
||||||
CateringMenu.RowView rolls = catering.menu().packages().get(0).rows().get(2);
|
CateringMenu.PackageView fresh = catering.add("Trays");
|
||||||
assertThat(rolls.label()).isEqualTo("Mini cinnamon rolls");
|
catering.save(fresh.id(), new CateringMenu.PackageEdit("Trays", null,
|
||||||
assertThat(rolls.values()).containsExactly("", "", "");
|
List.of(new CateringMenu.TierEdit(null, "Small", "10", null),
|
||||||
|
new CateringMenu.TierEdit(null, "Large", "20", null)),
|
||||||
|
List.of(new CateringMenu.RowEdit(null, "Seasonal extras", List.of("", "A dozen"))),
|
||||||
|
List.of()));
|
||||||
|
|
||||||
|
CateringMenu.RowView row = catering.everything().packages().getLast().rows().getFirst();
|
||||||
|
assertThat(row.values()).containsExactly("", "A dozen");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -185,7 +197,7 @@ class CateringMenuTest {
|
|||||||
CateringMenu.PackageView saved = catering.everything().packages().get(0);
|
CateringMenu.PackageView saved = catering.everything().packages().get(0);
|
||||||
assertThat(saved.tiers()).extracting(CateringMenu.TierView::label).containsExactly("Small", "Large");
|
assertThat(saved.tiers()).extracting(CateringMenu.TierView::label).containsExactly("Small", "Large");
|
||||||
assertThat(saved.tiers()).extracting(CateringMenu.TierView::id).doesNotContain(medium);
|
assertThat(saved.tiers()).extracting(CateringMenu.TierView::id).doesNotContain(medium);
|
||||||
assertThat(saved.rows().get(0).values()).containsExactly("A dozen", "Two dozen");
|
assertThat(saved.rows().get(0).values()).containsExactly("12 items", "24 items");
|
||||||
assertThat(saved.rows()).allSatisfy(row -> assertThat(row.values()).hasSize(2));
|
assertThat(saved.rows()).allSatisfy(row -> assertThat(row.values()).hasSize(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +213,7 @@ class CateringMenuTest {
|
|||||||
|
|
||||||
assertThatThrownBy(() -> catering.save(office.id(), half))
|
assertThatThrownBy(() -> catering.save(office.id(), half))
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessageContaining("Mini muffins")
|
.hasMessageContaining("Any mix of mini muffins")
|
||||||
.hasMessageContaining("3 entries but the table has 2 columns");
|
.hasMessageContaining("3 entries but the table has 2 columns");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +223,7 @@ class CateringMenuTest {
|
|||||||
Long muffins = office.rows().get(0).id();
|
Long muffins = office.rows().get(0).id();
|
||||||
|
|
||||||
List<CateringMenu.RowEdit> rows = new ArrayList<>(asLines(office));
|
List<CateringMenu.RowEdit> rows = new ArrayList<>(asLines(office));
|
||||||
rows.set(0, new CateringMenu.RowEdit(muffins, "Mini muffins", List.of("A dozen", "Twenty", "Two dozen")));
|
rows.set(0, new CateringMenu.RowEdit(muffins, "Mini muffins", List.of("12 items", "20 items", "24 items")));
|
||||||
catering.save(office.id(), new CateringMenu.PackageEdit(
|
catering.save(office.id(), new CateringMenu.PackageEdit(
|
||||||
"Office boxes", "For meetings and staff mornings.", asEdits(office), rows, office.notes()));
|
"Office boxes", "For meetings and staff mornings.", asEdits(office), rows, office.notes()));
|
||||||
|
|
||||||
@@ -220,7 +232,7 @@ class CateringMenuTest {
|
|||||||
assertThat(saved.blurb()).isEqualTo("For meetings and staff mornings.");
|
assertThat(saved.blurb()).isEqualTo("For meetings and staff mornings.");
|
||||||
// Same line, edited — not a new line that happens to read the same.
|
// Same line, edited — not a new line that happens to read the same.
|
||||||
assertThat(saved.rows().get(0).id()).isEqualTo(muffins);
|
assertThat(saved.rows().get(0).id()).isEqualTo(muffins);
|
||||||
assertThat(saved.rows().get(0).values()).containsExactly("A dozen", "Twenty", "Two dozen");
|
assertThat(saved.rows().get(0).values()).containsExactly("12 items", "20 items", "24 items");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -234,7 +246,7 @@ class CateringMenuTest {
|
|||||||
|
|
||||||
// Once it has a column and a line, it's a price table and it belongs on the page.
|
// Once it has a column and a line, it's a price table and it belongs on the page.
|
||||||
catering.save(fresh.id(), new CateringMenu.PackageEdit("Holiday boxes", null,
|
catering.save(fresh.id(), new CateringMenu.PackageEdit("Holiday boxes", null,
|
||||||
List.of(new CateringMenu.TierEdit(null, "Dozen", "18")),
|
List.of(new CateringMenu.TierEdit(null, "Dozen", "18", "About 6 people")),
|
||||||
List.of(new CateringMenu.RowEdit(null, "Frosted cut-outs", List.of("12 items"))),
|
List.of(new CateringMenu.RowEdit(null, "Frosted cut-outs", List.of("12 items"))),
|
||||||
List.of()));
|
List.of()));
|
||||||
assertThat(catering.menu().packages()).extracting(CateringMenu.PackageView::name)
|
assertThat(catering.menu().packages()).extracting(CateringMenu.PackageView::name)
|
||||||
@@ -304,7 +316,7 @@ class CateringMenuTest {
|
|||||||
/** Note the round trip: what came back as "$24" goes out again as "$24" and must still mean 2400. */
|
/** Note the round trip: what came back as "$24" goes out again as "$24" and must still mean 2400. */
|
||||||
private static List<CateringMenu.TierEdit> asEdits(CateringMenu.PackageView table) {
|
private static List<CateringMenu.TierEdit> asEdits(CateringMenu.PackageView table) {
|
||||||
return table.tiers().stream()
|
return table.tiers().stream()
|
||||||
.map(t -> new CateringMenu.TierEdit(t.id(), t.label(), t.price()))
|
.map(t -> new CateringMenu.TierEdit(t.id(), t.label(), t.price(), t.serves()))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,13 +340,13 @@ class CateringMenuTest {
|
|||||||
|
|
||||||
private static CateringMenu.PackageEdit withColumnPrice(CateringMenu.PackageView table, String price) {
|
private static CateringMenu.PackageEdit withColumnPrice(CateringMenu.PackageView table, String price) {
|
||||||
List<CateringMenu.TierEdit> tiers = new ArrayList<>(asEdits(table));
|
List<CateringMenu.TierEdit> tiers = new ArrayList<>(asEdits(table));
|
||||||
tiers.set(0, new CateringMenu.TierEdit(tiers.get(0).id(), tiers.get(0).label(), price));
|
tiers.set(0, new CateringMenu.TierEdit(tiers.get(0).id(), tiers.get(0).label(), price, null));
|
||||||
return new CateringMenu.PackageEdit(table.name(), table.blurb(), tiers, asLines(table), table.notes());
|
return new CateringMenu.PackageEdit(table.name(), table.blurb(), tiers, asLines(table), table.notes());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CateringMenu.PackageEdit withColumnLabel(CateringMenu.PackageView table, String label) {
|
private static CateringMenu.PackageEdit withColumnLabel(CateringMenu.PackageView table, String label) {
|
||||||
List<CateringMenu.TierEdit> tiers = new ArrayList<>(asEdits(table));
|
List<CateringMenu.TierEdit> tiers = new ArrayList<>(asEdits(table));
|
||||||
tiers.set(0, new CateringMenu.TierEdit(tiers.get(0).id(), label, tiers.get(0).price()));
|
tiers.set(0, new CateringMenu.TierEdit(tiers.get(0).id(), label, tiers.get(0).price(), null));
|
||||||
return new CateringMenu.PackageEdit(table.name(), table.blurb(), tiers, asLines(table), table.notes());
|
return new CateringMenu.PackageEdit(table.name(), table.blurb(), tiers, asLines(table), table.notes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,8 +102,11 @@ class SiteControllerTest {
|
|||||||
.andExpect(content().string(containsString("$24")))
|
.andExpect(content().string(containsString("$24")))
|
||||||
.andExpect(content().string(containsString("$236")))
|
.andExpect(content().string(containsString("$236")))
|
||||||
// A cell and a note, in the wording a customer reads rather than the spreadsheet's.
|
// A cell and a note, in the wording a customer reads rather than the spreadsheet's.
|
||||||
.andExpect(content().string(containsString("Eighteen, in two or three flavors")))
|
// The office box is a total mixed in sixes, not three separate things.
|
||||||
.andExpect(content().string(containsString("baked in sixes")))
|
.andExpect(content().string(containsString("Any mix of mini muffins")))
|
||||||
|
.andExpect(content().string(containsString("18 items")))
|
||||||
|
.andExpect(content().string(containsString("About 6–8 people")))
|
||||||
|
.andExpect(content().string(containsString("Baked in sixes")))
|
||||||
// One card per size, and one enquiry link per table — not one per card, which would have
|
// One card per size, and one enquiry link per table — not one per card, which would have
|
||||||
// read "Ask about the 15–20 people".
|
// read "Ask about the 15–20 people".
|
||||||
// The size label is in the HTML as written; the small caps are CSS.
|
// The size label is in the HTML as written; the small caps are CSS.
|
||||||
|
|||||||
Reference in New Issue
Block a user