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
26 changes: 13 additions & 13 deletions mcp/resources.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { fetchAllProductsFromDb } from "@/utils/db/db-service";
import {
getEffectiveShippingCost,
parseShippingFromTags,
} from "@/utils/parsers/product-tag-helpers";
import { NostrEvent } from "@/utils/types/types";

function getTagValue(tags: string[][], key: string): string | undefined {
Expand All @@ -17,21 +21,17 @@ function getAllTagValues(tags: string[][], key: string): string[] {
function buildCatalogEntry(event: NostrEvent) {
const tags = event.tags || [];
const priceTag = tags.find((t) => t[0] === "price");
const shippingTag = tags.find((t) => t[0] === "shipping");
const parsedShipping = parseShippingFromTags(tags);

const price = priceTag ? Number(priceTag[1]) : 0;
const currency = priceTag ? priceTag[2] || "" : "";
const shippingType = shippingTag ? shippingTag[1] || "" : "";
const shippingCost =
shippingTag && shippingTag[2] ? Number(shippingTag[2]) : 0;

const effectiveShippingCost =
shippingType === "Free" ||
shippingType === "Free/Pickup" ||
shippingType === "Pickup" ||
shippingType === "N/A"
? 0
: shippingCost;
const shippingType = parsedShipping?.shippingType;
const shippingCost = parsedShipping?.shippingCost;
const effectiveShippingCost = getEffectiveShippingCost(
shippingType,
shippingCost
);
const shippingCostForTotal = effectiveShippingCost ?? 0;

return {
id: event.id,
Expand All @@ -48,7 +48,7 @@ function buildCatalogEntry(event: NostrEvent) {
unit: "per item",
shippingCost: effectiveShippingCost,
shippingType: shippingType || "N/A",
totalEstimate: price + effectiveShippingCost,
totalEstimate: price + shippingCostForTotal,
paymentMethods: ["lightning", "cashu"],
},
};
Expand Down
29 changes: 15 additions & 14 deletions mcp/tools/read-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
validateDiscountCode,
getDbPool,
} from "@/utils/db/db-service";
import {
getEffectiveShippingCost,
parseShippingFromTags,
} from "@/utils/parsers/product-tag-helpers";
import { NostrEvent } from "@/utils/types/types";
import { registerTool } from "./register-tool";

Expand Down Expand Up @@ -36,39 +40,36 @@ function determinePaymentMethods(
function buildPricingBlock(
price: number,
currency: string,
shippingType: string,
shippingCost: number,
shippingType?: string,
shippingCost?: number,
quantity: number = 1,
paymentMethods?: string[]
) {
const effectiveShippingCost =
shippingType === "Free" ||
shippingType === "Free/Pickup" ||
shippingType === "Pickup" ||
shippingType === "N/A"
? 0
: shippingCost;
const effectiveShippingCost = getEffectiveShippingCost(
shippingType,
shippingCost
);
const shippingCostForTotal = effectiveShippingCost ?? 0;
return {
amount: price,
currency: currency || "sats",
unit: "per item",
shippingCost: effectiveShippingCost,
shippingType: shippingType || "N/A",
totalEstimate: price * quantity + effectiveShippingCost,
totalEstimate: price * quantity + shippingCostForTotal,
paymentMethods: paymentMethods || ["lightning", "cashu"],
};
}

function parseProductEvent(event: NostrEvent) {
const tags = event.tags || [];
const priceTag = tags.find((t) => t[0] === "price");
const shippingTag = tags.find((t) => t[0] === "shipping");
const parsedShipping = parseShippingFromTags(tags);

const price = priceTag ? Number(priceTag[1]) : 0;
const currency = priceTag ? priceTag[2] || "" : "";
const shippingType = shippingTag ? shippingTag[1] || "" : "";
const shippingCost =
shippingTag && shippingTag[2] ? Number(shippingTag[2]) : 0;
const shippingType = parsedShipping?.shippingType;
const shippingCost = parsedShipping?.shippingCost;

const sizes = tags
.filter((t) => t[0] === "size" && t[1])
Expand Down
77 changes: 77 additions & 0 deletions utils/parsers/__tests__/product-parser-functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ jest.mock("@/components/utility-components/display-monetary-info", () => ({
}));

const mockedCalculateTotalCost = calculateTotalCost as jest.Mock;
const totalCostWithoutShipping = ({
price,
shippingCost,
}: {
price: number;
shippingCost?: number;
}) => price + (shippingCost ?? 0);

describe("parseTags", () => {
const baseEvent: NostrEvent = {
Expand Down Expand Up @@ -78,6 +85,76 @@ describe("parseTags", () => {
expect(result.shippingCost).toBe(10);
});

it("should ignore legacy 2-value shipping tags", () => {
mockedCalculateTotalCost.mockImplementation(totalCostWithoutShipping);

const event = {
...baseEvent,
tags: [
["price", "50", "USD"],
["shipping", "5", "USD"],
],
};
const result = parseTags(event)!;

expect(result.shippingType).toBeUndefined();
expect(result.shippingCost).toBeUndefined();
expect(result.totalCost).toBe(50);
});

it("should ignore legacy 1-value shipping tags", () => {
mockedCalculateTotalCost.mockImplementation(totalCostWithoutShipping);

const event = {
...baseEvent,
tags: [
["price", "50", "USD"],
["shipping", "Free"],
],
};
const result = parseTags(event)!;

expect(result.shippingType).toBeUndefined();
expect(result.shippingCost).toBeUndefined();
expect(result.totalCost).toBe(50);
});

it("should ignore malformed modern shipping tags with non-numeric cost", () => {
mockedCalculateTotalCost.mockImplementation(totalCostWithoutShipping);

const event = {
...baseEvent,
tags: [
["price", "50", "USD"],
["shipping", "Added Cost", "not-a-number", "USD"],
],
};
const result = parseTags(event)!;

expect(result.shippingType).toBeUndefined();
expect(result.shippingCost).toBeUndefined();
expect(result.totalCost).toBe(50);
});

it("should ignore malformed modern shipping tags with negative cost", () => {
mockedCalculateTotalCost.mockImplementation(totalCostWithoutShipping);

const event = {
...baseEvent,
tags: [
["price", "50", "USD"],
["shipping", "Added Cost", "-10", "USD"],
],
};
const result = parseTags(event)!;

expect(result.shippingType).toBeUndefined();
expect(result.shippingCost).toBeUndefined();
expect(result.totalCost).toBe(50);
});



it("should parse various content-warning tags as true", () => {
const event1 = { ...baseEvent, tags: [["content-warning"]] };
expect(parseTags(event1)!.contentWarning).toBe(true);
Expand Down
72 changes: 72 additions & 0 deletions utils/parsers/__tests__/product-tag-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
getEffectiveShippingCost,
parseShippingFromTags,
parseShippingTag,
} from "../product-tag-helpers";

describe("parseShippingTag", () => {
it("parses the modern 3-value shipping tag format", () => {
expect(
parseShippingTag(["shipping", "Added Cost", "10", "USD"])
).toEqual({
shippingType: "Added Cost",
shippingCost: 10,
});
});

it("ignores legacy 2-value shipping tags", () => {
expect(parseShippingTag(["shipping", "5", "USD"])).toBeUndefined();
});

it("ignores legacy 1-value shipping tags", () => {
expect(parseShippingTag(["shipping", "Free"])).toBeUndefined();
});

it("ignores malformed shipping tags with non-numeric cost", () => {
expect(
parseShippingTag(["shipping", "Added Cost", "not-a-number", "USD"])
).toBeUndefined();
});

it("ignores malformed shipping tags with negative cost", () => {
expect(
parseShippingTag(["shipping", "Added Cost", "-10", "USD"])
).toBeUndefined();
});
});

describe("getEffectiveShippingCost", () => {
it("returns null when shipping metadata is missing", () => {
expect(getEffectiveShippingCost(undefined, undefined)).toBeNull();
});

it("returns zero for non-paid shipping types", () => {
expect(getEffectiveShippingCost("Free", 15)).toBe(0);
expect(getEffectiveShippingCost("Free/Pickup", 15)).toBe(0);
expect(getEffectiveShippingCost("Pickup", 15)).toBe(0);
expect(getEffectiveShippingCost("N/A", 15)).toBe(0);
});

it("returns null when a paid shipping cost is invalid", () => {
expect(getEffectiveShippingCost("Added Cost", Number.NaN)).toBeNull();
expect(getEffectiveShippingCost("Added Cost", -15)).toBeNull();
});

it("returns the parsed cost for paid shipping", () => {
expect(getEffectiveShippingCost("Added Cost", 15)).toBe(15);
});
});

describe("parseShippingFromTags", () => {
it("accepts a later valid modern shipping tag after legacy tags", () => {
expect(
parseShippingFromTags([
["shipping", "5", "USD"],
["shipping", "Added Cost", "12", "USD"],
])
).toEqual({
shippingType: "Added Cost",
shippingCost: 12,
});
});
});
11 changes: 5 additions & 6 deletions utils/parsers/product-parser-functions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ShippingOptionsType } from "@/utils/STATIC-VARIABLES";
import { calculateTotalCost } from "@/components/utility-components/display-monetary-info";
import { parseShippingTag } from "@/utils/parsers/product-tag-helpers";
import { NostrEvent } from "@/utils/types/types";

export type ProductData = {
Expand Down Expand Up @@ -94,13 +95,11 @@ export const parseTags = (productEvent: NostrEvent) => {
parsedData.currency = currency!;
break;
case "shipping":
if (values.length === 3) {
const [shippingType, cost, _currency] = values;
parsedData.shippingType = shippingType as ShippingOptionsType;
parsedData.shippingCost = Number(cost);
break;
const parsedShipping = parseShippingTag(tag);
if (parsedShipping) {
parsedData.shippingType = parsedShipping.shippingType;
parsedData.shippingCost = parsedShipping.shippingCost;
}

break;
case "d":
parsedData.d = values[0];
Expand Down
85 changes: 85 additions & 0 deletions utils/parsers/product-tag-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
SHIPPING_OPTIONS,
ShippingOptionsType,
} from "@/utils/STATIC-VARIABLES";

export type ParsedShippingTag = {
shippingType: ShippingOptionsType;
shippingCost: number;
};

export function parseShippingTag(
tag?: string[]
): ParsedShippingTag | undefined {
if (!tag || tag[0] !== "shipping" || tag.length !== 4) {
return;
}

const [, shippingType, rawShippingCost, shippingCurrency] = tag;

if (
!shippingType ||
!shippingCurrency ||
!SHIPPING_OPTIONS.includes(shippingType as ShippingOptionsType)
) {
return;
}

if (!rawShippingCost?.trim()) {
return;
}

const shippingCost = Number(rawShippingCost);
if (!Number.isFinite(shippingCost) || shippingCost < 0) {
return;
}

return {
shippingType: shippingType as ShippingOptionsType,
shippingCost,
};
}

export function parseShippingFromTags(
tags: string[][]
): ParsedShippingTag | undefined {
let parsedShipping: ParsedShippingTag | undefined;

for (const tag of tags) {
if (tag[0] !== "shipping") continue;

const parsed = parseShippingTag(tag);
if (parsed) {
parsedShipping = parsed;
}
}

return parsedShipping;
}

export function getEffectiveShippingCost(
shippingType?: string,
shippingCost?: number
): number | null {
if (!shippingType) {
return null;
}
if (
shippingType === "Free" ||
shippingType === "Free/Pickup" ||
shippingType === "Pickup" ||
shippingType === "N/A"
) {
return 0;
}

if (
typeof shippingCost !== "number" ||
!Number.isFinite(shippingCost) ||
shippingCost < 0
) {
return null;
}

return shippingCost;
}