diff --git a/src/main/resources/db/migration/V6__catering_copy.sql b/src/main/resources/db/migration/V6__catering_copy.sql new file mode 100644 index 0000000..19e4323 --- /dev/null +++ b/src/main/resources/db/migration/V6__catering_copy.sql @@ -0,0 +1,126 @@ +-- The catering tables in the bakery's voice instead of the spreadsheet's shorthand. +-- +-- V4 kept the source wording deliberately: it was a spreadsheet written for the people who bake from it, +-- and inventing copy for somebody else's prices is not a migration's job. But the page is read by +-- customers, and "B&G cake", "2 pans", "4 dz" and "6+6" are notes-to-self, not an offer. So this rewrites +-- the cells as sentences, fills in the blurb each table always had room for, and merges the parties +-- lines the same way V5 merged the wedding ones — "Cupcakes" quantified as "1 dz sugar cookies or +-- cupcakes" with an empty "Sugar cookies" line beneath it is the same offset, and the same fix. +-- +-- Guarded on the exact text V4 and V5 left, as V5 was: anything the bakery has already reworded in the +-- admin is left exactly as they wrote it. Their wording wins over mine. + +-- --- names and the line under each heading ---------------------------------------------------------- + +update catering_package set name = 'Office boxes' + where name = 'Office'; + +update catering_package set blurb = 'Mini pastries for a morning meeting, boxed and ready to collect.' + where name = 'Office boxes' and blurb is null; + +update catering_package set blurb = 'A cake, and something to hand round, sized to your guest list.' + where name = 'Parties' and blurb is null; + +update catering_package set blurb = 'A cake for the couple, and dessert for everyone else.' + where name = 'Weddings' and blurb is null; + +-- --- office boxes ----------------------------------------------------------------------------------- + +update catering_row_value v set value = 'A dozen' + from catering_row r where r.id = v.row_id and r.label = 'Mini muffins' and v.value = '12 items'; +update catering_row_value v set value = 'Eighteen' + from catering_row r where r.id = v.row_id and r.label = 'Mini muffins' and v.value = '18 items'; +update catering_row_value v set value = 'Two dozen' + from catering_row r where r.id = v.row_id and r.label = 'Mini muffins' and v.value = '24 items'; + +-- "6+6" is six of one flavour and six of another, which is worth saying out loud. +update catering_row_value v set value = 'A dozen, in two flavors' + from catering_row r where r.id = v.row_id and r.label = 'Mini scones' and v.value = '6+6'; +update catering_row_value v set value = 'Eighteen, in two or three flavors' + from catering_row r where r.id = v.row_id and r.label = 'Mini scones' and v.value = '6+6+6 or 12+6'; +update catering_row_value v set value = 'Two dozen, in up to four flavors' + from catering_row r where r.id = v.row_id and r.label = 'Mini scones' + and v.value = '6+6+6+6 or 12+6+6 or 12+12'; + +update catering_package_note set body = 'Everything is baked in sixes, so each item comes in multiples of six.' + where body = 'Minimum of 6 items per baked good. Flavors can''t be mixed and matched unless you''re ordering a large quantity.'; + +insert into catering_package_note (package_id, position, body) +select p.id, 1, 'Mixing flavors within one item needs a larger order — ask us and we will tell you.' + from catering_package p + where p.name = 'Office boxes' + and not exists (select 1 from catering_package_note n where n.package_id = p.id and n.position = 1); + +-- --- parties ---------------------------------------------------------------------------------------- + +update catering_row_value v set value = 'A 6-inch cake' + from catering_row r where r.id = v.row_id and r.label = 'Cake' and v.value = '6 in cake'; +update catering_row_value v set value = 'An 8-inch cake' + from catering_row r where r.id = v.row_id and r.label = 'Cake' and v.value = '8 in cake'; +update catering_row_value v set value = 'A 10-inch cake' + from catering_row r where r.id = v.row_id and r.label = 'Cake' and v.value = '10 in cake'; + +-- Same offset as the wedding table: the quantities on the "Cupcakes" line were always +-- cupcakes-or-sugar-cookies, and the "Sugar cookies" line below carried nothing at all. +update catering_row r set label = 'Cupcakes or sugar cookies' + from catering_package p + where p.id = r.package_id and p.name = 'Parties' and r.label = 'Cupcakes'; + +update catering_row_value v set value = 'A dozen, either one' + from catering_row r where r.id = v.row_id and r.label = 'Cupcakes or sugar cookies' + and v.value = '1 dz sugar cookies or cupcakes'; +update catering_row_value v set value = 'Eighteen, your choice' + from catering_row r where r.id = v.row_id and r.label = 'Cupcakes or sugar cookies' + and v.value = '1.5 dz your choice'; +update catering_row_value v set value = 'Two dozen, your choice' + from catering_row r where r.id = v.row_id and r.label = 'Cupcakes or sugar cookies' + and v.value = '2 dz your choice'; + +delete from catering_row r + using catering_package p + where p.id = r.package_id and p.name = 'Parties' and r.label = 'Sugar cookies' + and not exists (select 1 from catering_row_value v where v.row_id = r.id and v.value <> ''); + +update catering_package_note set body = 'An extra dozen is $20.' + where body = 'Add an extra dozen for $20.'; +update catering_package_note + set body = 'Cake and cupcake flavors can be different once you are ordering a dozen cupcakes or more.' + where body = 'Cake and cupcake flavors can''t be mixed unless you order at least 1 dz of cupcakes.'; + +-- --- weddings --------------------------------------------------------------------------------------- + +-- The label said it and the cell repeated it. The cell now carries the size instead. +update catering_row r set label = 'Bride & groom cake' + from catering_package p + where p.id = r.package_id and p.name = 'Weddings' and r.label = 'Bride & groom cake (8 in)'; + +update catering_row_value v set value = 'Eight inch' + from catering_row r where r.id = v.row_id and r.label = 'Bride & groom cake' and v.value = 'B&G cake'; + +update catering_row r set label = 'Sheet cakes or 12×17 bars' + from catering_package p + where p.id = r.package_id and p.name = 'Weddings' and r.label = 'Sheet cakes or 12x17 bars'; + +update catering_row_value v set value = 'Two pans' + from catering_row r where r.id = v.row_id and r.label = 'Sheet cakes or 12×17 bars' and v.value = '2 pans'; +update catering_row_value v set value = 'Three pans' + from catering_row r where r.id = v.row_id and r.label = 'Sheet cakes or 12×17 bars' and v.value = '3 pans'; +update catering_row_value v set value = 'Four pans' + from catering_row r where r.id = v.row_id and r.label = 'Sheet cakes or 12×17 bars' and v.value = '4 pans'; + +update catering_row_value v set value = 'Four dozen' + from catering_row r where r.id = v.row_id and r.label = 'Cupcakes or sugar cookies' and v.value = '4 dz'; +update catering_row_value v set value = 'Five dozen' + from catering_row r where r.id = v.row_id and r.label = 'Cupcakes or sugar cookies' and v.value = '5 dz'; +update catering_row_value v set value = 'Six dozen' + from catering_row r where r.id = v.row_id and r.label = 'Cupcakes or sugar cookies' and v.value = '6 dz'; + +-- --- the page's own terms --------------------------------------------------------------------------- + +update catering_note + set body = 'These are a guide rather than a menu. We are happy to make changes, though the price may change with them.' + where body = 'We''re happy to make changes — the price may change with them.'; + +update catering_note + set body = 'If we can''t do something, we will tell you.' + where body = 'If we can''t do something we''ll tell you. These tables are mostly here to give you an idea of what''s possible.'; diff --git a/src/main/resources/templates/catering.html b/src/main/resources/templates/catering.html index dac1b04..29708d2 100644 --- a/src/main/resources/templates/catering.html +++ b/src/main/resources/templates/catering.html @@ -4,9 +4,9 @@
-

To take away

+

Order ahead

Goodie boxes & catering

Boxes for the office, packages for a party, and cakes and desserts for a wedding. @@ -30,7 +30,7 @@

Every one of these is a starting point. Tell us the date, the number of people and what you had in - mind, and we will work from it. + mind, and we will bake to it.

-
- -
-

Office

-

Blurb

+
+
+

Office boxes

+

+ Mini pastries for a morning meeting. +

- - + +
+
+

Small

+

$24

+ +

Ask us

- -
-
-
-

Small

- $24 - Ask us -
- -
-
-
Mini muffins
-
- 12 items - -
-
-
-
+
    +
  • + + + + Mini muffins + + — a dozen + +
  • +
+ +
-
-
+
+

Good to know

-
    -
  • Minimum of 6 items per baked good.
  • +
      +
    • Everything is baked in sixes.
- - Ask about office - - + class="mt-6 inline-block border border-bakery-300 hover:bg-bakery-100 text-bakery-800 + px-7 py-3 rounded-full font-medium tracking-wide transition-colors" + th:text="|Ask about ${#strings.toLowerCase(table.name)}|">Ask about office boxes
@@ -160,7 +133,7 @@

Before you order

    -
  • Prices may change.
  • +
  • These are a guide rather than a menu.
tables = catering.menu().packages(); assertThat(tables).extracting(CateringMenu.PackageView::name) - .containsExactly("Office", "Parties", "Weddings"); + .containsExactly("Office boxes", "Parties", "Weddings"); CateringMenu.PackageView office = tables.get(0); assertThat(office.tiers()).extracting(CateringMenu.TierView::label) @@ -69,9 +71,10 @@ class CateringMenuTest { .containsExactly("$24", "$32", "$40"); assertThat(office.rows()).extracting(CateringMenu.RowView::label) .containsExactly("Mini muffins", "Mini scones", "Mini cinnamon rolls"); - assertThat(office.rows().get(0).values()).containsExactly("12 items", "18 items", "24 items"); - assertThat(office.rows().get(1).values().get(2)).isEqualTo("6+6+6+6 or 12+6+6 or 12+12"); - assertThat(office.notes()).singleElement().asString().contains("Minimum of 6 items per baked good"); + assertThat(office.rows().get(0).values()).containsExactly("A dozen", "Eighteen", "Two dozen"); + assertThat(office.rows().get(1).values().get(2)).isEqualTo("Two dozen, in up to four flavors"); + assertThat(office.blurb()).contains("morning meeting"); + assertThat(office.notes()).anySatisfy(note -> assertThat(note).contains("baked in sixes")); assertThat(tables.get(1).tiers()).extracting(CateringMenu.TierView::label) .containsExactly("15–20 people", "20–30 people", "30–40 people"); @@ -79,6 +82,9 @@ class CateringMenuTest { .containsExactly("$236", "$310", "$386"); // Wedding delivery terms belong to the wedding table, not to the page. assertThat(tables.get(2).notes()).anySatisfy(note -> assertThat(note).contains("delivery fee")); + // No cell anywhere still speaks in shorthand. + assertThat(tables).allSatisfy(table -> assertThat(table.rows()).allSatisfy(row -> + assertThat(row.values()).noneMatch(value -> value.matches(".*\\b(dz|B&G|in cake)\\b.*")))); } @Test @@ -90,15 +96,28 @@ class CateringMenuTest { CateringMenu.PackageView weddings = catering.menu().packages().get(2); assertThat(weddings.rows()).extracting(CateringMenu.RowView::label) - .containsExactly("Bride & groom cake (8 in)", "Sheet cakes or 12x17 bars", + .containsExactly("Bride & groom cake", "Sheet cakes or 12×17 bars", "Cupcakes or sugar cookies"); - assertThat(weddings.rows().get(1).values()).containsExactly("2 pans", "3 pans", "4 pans"); - assertThat(weddings.rows().get(2).values()).containsExactly("4 dz", "5 dz", "6 dz"); + assertThat(weddings.rows().get(1).values()).containsExactly("Two pans", "Three pans", "Four pans"); + assertThat(weddings.rows().get(2).values()).containsExactly("Four dozen", "Five dozen", "Six dozen"); // Nothing left that is blank the whole way across. assertThat(weddings.rows()) .noneMatch(row -> row.values().stream().allMatch(String::isEmpty)); } + @Test + void thePartyLinesEachSaySomethingToo() { + // Same offset as the wedding table: the quantities on "Cupcakes" were always + // cupcakes-or-sugar-cookies, with an empty "Sugar cookies" line beneath. + CateringMenu.PackageView parties = catering.menu().packages().get(1); + assertThat(parties.rows()).extracting(CateringMenu.RowView::label) + .containsExactly("Cake", "Cupcakes or sugar cookies"); + assertThat(parties.rows().get(0).values()) + .containsExactly("A 6-inch cake", "An 8-inch cake", "A 10-inch cake"); + assertThat(parties.rows()) + .noneMatch(row -> row.values().stream().allMatch(String::isEmpty)); + } + @Test void everyLineCarriesOneEntryPerColumn() { // The invariant the whole aggregate exists to hold: if these ever fall out of step, a box is @@ -166,7 +185,7 @@ class CateringMenuTest { CateringMenu.PackageView saved = catering.everything().packages().get(0); assertThat(saved.tiers()).extracting(CateringMenu.TierView::label).containsExactly("Small", "Large"); assertThat(saved.tiers()).extracting(CateringMenu.TierView::id).doesNotContain(medium); - assertThat(saved.rows().get(0).values()).containsExactly("12 items", "24 items"); + assertThat(saved.rows().get(0).values()).containsExactly("A dozen", "Two dozen"); assertThat(saved.rows()).allSatisfy(row -> assertThat(row.values()).hasSize(2)); } @@ -192,7 +211,7 @@ class CateringMenuTest { Long muffins = office.rows().get(0).id(); List rows = new ArrayList<>(asLines(office)); - rows.set(0, new CateringMenu.RowEdit(muffins, "Mini muffins", List.of("12 items", "20 items", "24 items"))); + rows.set(0, new CateringMenu.RowEdit(muffins, "Mini muffins", List.of("A dozen", "Twenty", "Two dozen"))); catering.save(office.id(), new CateringMenu.PackageEdit( "Office boxes", "For meetings and staff mornings.", asEdits(office), rows, office.notes())); @@ -201,7 +220,7 @@ class CateringMenuTest { assertThat(saved.blurb()).isEqualTo("For meetings and staff mornings."); // 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).values()).containsExactly("12 items", "20 items", "24 items"); + assertThat(saved.rows().get(0).values()).containsExactly("A dozen", "Twenty", "Two dozen"); } @Test @@ -209,7 +228,7 @@ class CateringMenuTest { CateringMenu.PackageView fresh = catering.add("Holiday boxes"); assertThat(catering.everything().packages()).extracting(CateringMenu.PackageView::name) - .containsExactly("Office", "Parties", "Weddings", "Holiday boxes"); + .containsExactly("Office boxes", "Parties", "Weddings", "Holiday boxes"); assertThat(catering.menu().packages()).extracting(CateringMenu.PackageView::name) .doesNotContain("Holiday boxes"); @@ -233,7 +252,7 @@ class CateringMenuTest { entityManager.clear(); assertThat(catering.everything().packages()).extracting(CateringMenu.PackageView::name) - .containsExactly("Office", "Parties"); + .containsExactly("Office boxes", "Parties"); // The page's own terms outlive any one table. assertThat(catering.menu().notes()).hasSize(2); } @@ -244,9 +263,9 @@ class CateringMenuTest { List weddingsFirst = List.of(tables.get(2).id(), tables.get(0).id(), tables.get(1).id()); assertThat(catering.reorder(weddingsFirst)).extracting(CateringMenu.PackageView::name) - .containsExactly("Weddings", "Office", "Parties"); + .containsExactly("Weddings", "Office boxes", "Parties"); assertThat(catering.menu().packages()).extracting(CateringMenu.PackageView::name) - .containsExactly("Weddings", "Office", "Parties"); + .containsExactly("Weddings", "Office boxes", "Parties"); } @Test diff --git a/src/test/java/com/itsthevine/web/SiteControllerTest.java b/src/test/java/com/itsthevine/web/SiteControllerTest.java index 78d33b2..95a3e44 100644 --- a/src/test/java/com/itsthevine/web/SiteControllerTest.java +++ b/src/test/java/com/itsthevine/web/SiteControllerTest.java @@ -96,16 +96,21 @@ class SiteControllerTest { @Test void theCateringTablesAreRenderedFromTheDatabase() throws Exception { mvc.perform(get("/catering")).andExpect(status().isOk()) - .andExpect(content().string(containsString("Office"))) + .andExpect(content().string(containsString("Office boxes"))) .andExpect(content().string(containsString("Weddings"))) // Prices as the server writes them — the page never formats money. .andExpect(content().string(containsString("$24"))) .andExpect(content().string(containsString("$236"))) - // A cell, and a note. - .andExpect(content().string(containsString("6+6+6 or 12+6"))) - .andExpect(content().string(containsString("Minimum of 6 items per baked good"))) - // Both renderings of the same table are present; CSS decides which one is visible. - .andExpect(content().string(containsString("