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
87 changes: 87 additions & 0 deletions client/src/lib/spreadsheet/menuSpreadsheet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, it, expect } from "vitest";
import { buildSpreadsheetPreviewEvents } from "./menuSpreadsheet";

const MERCHANT_PUBKEY = "npub1merchant000000000000000000000000000000000000000000000000";

describe("buildSpreadsheetPreviewEvents", () => {
it("emits two 30402 events with distinct d-tags when same item is in different submenus", () => {
const menus = [
{ Name: "Lunch & Dinner", "Parent Menu": "", "Menu Type": "menu", Description: "Lunch & Dinner Menu" },
{ Name: "Brunch", "Parent Menu": "", "Menu Type": "menu", Description: "Brunch Menu" },
{ Name: "Sandos", "Parent Menu": "Lunch & Dinner", Description: "Sandos section" },
{ Name: "The 'Unch Part", "Parent Menu": "Brunch", Description: "The Unch Part section" },
];
const items = [
{
Name: "Bigfoot Burger",
Description: "A big burger",
"Part of Menu": "Lunch & Dinner",
"Part of Menu Section": "Sandos",
},
{
Name: "Bigfoot Burger",
Description: "A big burger",
"Part of Menu": "Brunch",
"Part of Menu Section": "The 'Unch Part",
},
];

const events = buildSpreadsheetPreviewEvents({
merchantPubkey: MERCHANT_PUBKEY,
menus,
items,
});

const productEvents = events.filter((e) => e.kind === 30402);
expect(productEvents).toHaveLength(2);

const dTags = productEvents.map((e) => e.tags.find((t) => t[0] === "d")?.[1]).filter(Boolean);
expect(dTags).toHaveLength(2);
expect(dTags[0]).not.toBe(dTags[1]);
expect(new Set(dTags).size).toBe(2);
});

it("emits two 30402 events with distinct d-tags when two rows have identical name, menu, and section", () => {
const menus = [
{ Name: "Lunch", "Parent Menu": "", "Menu Type": "menu", Description: "Lunch" },
{ Name: "Mains", "Parent Menu": "Lunch", Description: "Mains section" },
];
const items = [
{ Name: "Caesar Salad", Description: "Salad", "Part of Menu": "Lunch", "Part of Menu Section": "Mains" },
{ Name: "Caesar Salad", Description: "Salad", "Part of Menu": "Lunch", "Part of Menu Section": "Mains" },
];

const events = buildSpreadsheetPreviewEvents({
merchantPubkey: MERCHANT_PUBKEY,
menus,
items,
});

const productEvents = events.filter((e) => e.kind === 30402);
expect(productEvents).toHaveLength(2);

const dTags = productEvents.map((e) => e.tags.find((t) => t[0] === "d")?.[1]).filter(Boolean);
expect(dTags).toHaveLength(2);
expect(dTags[0]).not.toBe(dTags[1]);
expect(dTags[1]).toMatch(/-2$/);
});

it("emits one 30402 event with slug(name) d-tag when single row has no menu or section", () => {
const menus: Array<Record<string, string>> = [];
const items = [
{ Name: "House Coffee", Description: "Fresh brew" },
];

const events = buildSpreadsheetPreviewEvents({
merchantPubkey: MERCHANT_PUBKEY,
menus,
items,
});

const productEvents = events.filter((e) => e.kind === 30402);
expect(productEvents).toHaveLength(1);

const dTag = productEvents[0].tags.find((t) => t[0] === "d")?.[1];
expect(dTag).toBe("house-coffee");
});
});
57 changes: 48 additions & 9 deletions client/src/lib/spreadsheet/menuSpreadsheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,37 @@ function firstNonEmpty(...values: Array<string | undefined | null>): string {
return "";
}

/**
* Computes a d-tag for a menu item row so that the same item in different
* menus/sections gets a distinct identifier (supporting one 30402 event per row).
*/
function computeItemDTag(
name: string,
menu: string,
section: string,
fallbackIndex: number
): string {
const context = [section, menu].filter(Boolean).join("-");
const base = context
? (slugify(name) && slugify(context) ? `${slugify(name)}-${slugify(context)}` : slugify(name))
: slugify(name);
return base || `item-${fallbackIndex + 1}`;
}

/** Ensures each d-tag is unique; duplicates get -2, -3, ... suffix. */
function uniquifyDTags(baseDTags: string[]): string[] {
const used = new Set<string>();
return baseDTags.map((d) => {
let s = d;
let c = 1;
while (used.has(s)) {
s = `${d}-${++c}`;
}
used.add(s);
return s;
});
}

export async function parseMenuSpreadsheetXlsx(file: File): Promise<{
menus: MenuRow[];
items: MenuItemRow[];
Expand Down Expand Up @@ -107,19 +138,27 @@ export function buildSpreadsheetPreviewEvents(params: {
titleByMenuName.set(name, description);
}

// Build 30402 items first and keep a map name -> dTag for collection a-tags.
const baseDTags = items.map((row, i) =>
computeItemDTag(
asString(row.Name),
asString((row as any)["Part of Menu"]),
asString((row as any)["Part of Menu Section"]),
i
)
);
const finalDTags = uniquifyDTags(baseDTags);

// Build 30402 items first; each row gets its own d-tag for distinct events per menu/section.
const itemEvents: SquareEventTemplate[] = [];
const dTagByItemName = new Map<string, string>();

for (const row of items) {
for (let i = 0; i < items.length; i++) {
const row = items[i];
const name = asString(row.Name);
if (!name) continue;
const description = asString(row.Description);
const content = `**${name}**\n\n${description}`.trim();

const dTag = slugify(name) || `item-${itemEvents.length + 1}`;
dTagByItemName.set(name, dTag);

const dTag = finalDTags[i];
const tags: string[][] = [];
tags.push(["d", dTag]);
tags.push(["title", name]);
Expand Down Expand Up @@ -180,11 +219,11 @@ export function buildSpreadsheetPreviewEvents(params: {

// Build membership map: collection name -> item dTags
const collectionToProductDTags = new Map<string, string[]>();
for (const row of items) {
for (let i = 0; i < items.length; i++) {
const row = items[i];
const name = asString(row.Name);
if (!name) continue;
const dTag = dTagByItemName.get(name);
if (!dTag) continue;
const dTag = finalDTags[i];

const menu = asString((row as any)["Part of Menu"]);
const section = asString((row as any)["Part of Menu Section"]);
Expand Down