diff --git a/mcp/tools/__tests__/read-tools.contract.test.ts b/mcp/tools/__tests__/read-tools.contract.test.ts new file mode 100644 index 000000000..d07fd4f15 --- /dev/null +++ b/mcp/tools/__tests__/read-tools.contract.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "@jest/globals"; +import { buildPricingBlock, parseProductEvent } from "@/mcp/tools/read-tools"; +import { NostrEvent } from "@/utils/types/types"; + +function makeProductEvent(overrides?: Partial): NostrEvent { + return { + id: "product-1", + pubkey: "seller-pubkey", + created_at: 1710000000, + kind: 30402, + content: "", + sig: "sig", + tags: [ + ["d", "tea-001"], + ["title", "Green Tea"], + ["summary", "Single origin"], + ["image", "https://example.com/tea.jpg"], + ["t", "beverages"], + ["location", "Online"], + ["price", "100", "USD"], + ["shipping", "Added Cost", "5", "USD"], + ["required_customer_info", "email"], + ["size", "M", "8"], + ["volume", "250g", "140"], + ["weight", "1 lb", "220"], + ["bulk", "10", "900"], + ["subscription", "true"], + ["subscription_discount", "10"], + ["subscription_frequency", "weekly", "monthly"], + ["published_at", "1700000000"], + ["valid_until", "1900000000"], + ["content-warning"], + ["restrictions", "US only"], + ], + ...overrides, + }; +} + +describe("read-tools MCP product contract helpers", () => { + it("parseProductEvent returns MCP-safe product shape with pricing and subscription", () => { + const parsed = parseProductEvent(makeProductEvent()); + + expect(parsed).toEqual( + expect.objectContaining({ + id: "product-1", + pubkey: "seller-pubkey", + d: "tea-001", + title: "Green Tea", + summary: "Single origin", + requiredCustomerInfo: "email", + publishedAt: "1700000000", + validUntil: 1900000000, + contentWarning: true, + }) + ); + + expect(parsed.pricing).toEqual({ + amount: 100, + currency: "USD", + unit: "per item", + shippingCost: 5, + shippingType: "Added Cost", + totalEstimate: 105, + paymentMethods: ["lightning", "cashu"], + }); + + expect(parsed.subscription).toEqual({ + enabled: true, + discount: 10, + frequencies: ["weekly", "monthly"], + }); + }); + + it("parseProductEvent maps legacy UI-only 'required' to MCP requiredCustomerInfo", () => { + const parsed = parseProductEvent( + makeProductEvent({ + tags: [ + ["title", "Legacy Product"], + ["price", "25", "USD"], + ["required", "email"], + ], + }) + ); + + expect(parsed.requiredCustomerInfo).toBe("email"); + }); + + it("buildPricingBlock keeps free shipping total estimate deterministic", () => { + const pricing = buildPricingBlock(120, "USD", "Free", 999, 2); + + expect(pricing).toEqual({ + amount: 120, + currency: "USD", + unit: "per item", + shippingCost: 0, + shippingType: "Free", + totalEstimate: 240, + paymentMethods: ["lightning", "cashu"], + }); + }); +}); diff --git a/mcp/tools/read-tools.ts b/mcp/tools/read-tools.ts index 991239767..b69337b14 100644 --- a/mcp/tools/read-tools.ts +++ b/mcp/tools/read-tools.ts @@ -7,10 +7,9 @@ import { validateDiscountCode, getDbPool, } from "@/utils/db/db-service"; -import { - getEffectiveShippingCost, - parseShippingFromTags, -} from "@/utils/parsers/product-tag-helpers"; +import { getEffectiveShippingCost } from "@/utils/parsers/product-tag-helpers"; +import { parseCanonicalProductEvent } from "@/utils/parsers/product-event/base-parser"; +import { toMcpProductData } from "@/utils/parsers/product-event/mcp-adapter"; import { NostrEvent } from "@/utils/types/types"; import { registerTool } from "./register-tool"; @@ -19,13 +18,6 @@ function getTagValue(tags: string[][], key: string): string | undefined { return tag ? tag[1] : undefined; } -function getAllTagValues(tags: string[][], key: string): string[] { - return tags - .filter((t) => t[0] === key) - .map((t) => t[1]!) - .filter(Boolean); -} - function determinePaymentMethods( _sellerPubkey?: string, hasStripeConnect?: boolean @@ -37,7 +29,7 @@ function determinePaymentMethods( return methods; } -function buildPricingBlock( +export function buildPricingBlock( price: number, currency: string, shippingType?: string, @@ -61,70 +53,18 @@ function buildPricingBlock( }; } -function parseProductEvent(event: NostrEvent) { - const tags = event.tags || []; - const priceTag = tags.find((t) => t[0] === "price"); - const parsedShipping = parseShippingFromTags(tags); - - const price = priceTag ? Number(priceTag[1]) : 0; - const currency = priceTag ? priceTag[2] || "" : ""; - const shippingType = parsedShipping?.shippingType; - const shippingCost = parsedShipping?.shippingCost; - - const sizes = tags - .filter((t) => t[0] === "size" && t[1]) - .map((t) => ({ size: t[1]!, quantity: t[2] ? Number(t[2]) : undefined })); - - const volumes = tags - .filter((t) => t[0] === "volume" && t[1]) - .map((t) => ({ volume: t[1]!, price: t[2] ? Number(t[2]) : undefined })); - - const weights = tags - .filter((t) => t[0] === "weight" && t[1]) - .map((t) => ({ weight: t[1]!, price: t[2] ? Number(t[2]) : undefined })); - - const bulk = tags - .filter((t) => t[0] === "bulk" && t[1] && t[2]) - .map((t) => ({ units: Number(t[1]), price: Number(t[2]) })); - - const pickupLocations = getAllTagValues(tags, "pickup_location"); +export function parseProductEvent(event: NostrEvent) { + const canonical = parseCanonicalProductEvent(event); + const parsed = toMcpProductData(canonical); return { - id: event.id, - pubkey: event.pubkey, - d: getTagValue(tags, "d"), - title: getTagValue(tags, "title") || "", - summary: getTagValue(tags, "summary") || "", - images: getAllTagValues(tags, "image"), - categories: getAllTagValues(tags, "t"), - location: getTagValue(tags, "location") || "", - price, - currency, - shippingType, - shippingCost, - quantity: getTagValue(tags, "quantity") - ? Number(getTagValue(tags, "quantity")) - : undefined, - condition: getTagValue(tags, "condition"), - status: getTagValue(tags, "status"), - sizes: sizes.length > 0 ? sizes : undefined, - volumes: volumes.length > 0 ? volumes : undefined, - weights: weights.length > 0 ? weights : undefined, - bulk: bulk.length > 0 ? bulk : undefined, - pickupLocations: pickupLocations.length > 0 ? pickupLocations : undefined, - requiredCustomerInfo: getTagValue(tags, "required_customer_info"), - createdAt: event.created_at, - pricing: buildPricingBlock(price, currency, shippingType, shippingCost), - subscription: { - enabled: getTagValue(tags, "subscription") === "true", - discount: getTagValue(tags, "subscription_discount") - ? Number(getTagValue(tags, "subscription_discount")) - : undefined, - frequencies: (() => { - const freqTag = tags.find((t) => t[0] === "subscription_frequency"); - return freqTag ? freqTag.slice(1) : []; - })(), - }, + ...parsed, + pricing: buildPricingBlock( + parsed.price, + parsed.currency, + parsed.shippingType, + parsed.shippingCost + ), }; } diff --git a/utils/parsers/__tests__/product-event-parser.test.ts b/utils/parsers/__tests__/product-event-parser.test.ts new file mode 100644 index 000000000..5d0fc35da --- /dev/null +++ b/utils/parsers/__tests__/product-event-parser.test.ts @@ -0,0 +1,81 @@ +import { parseCanonicalProductEvent } from "@/utils/parsers/product-event/base-parser"; +import { toMcpProductData } from "@/utils/parsers/product-event/mcp-adapter"; +import { NostrEvent } from "@/utils/types/types"; + +describe("product canonical parser + adapters", () => { + const baseEvent: NostrEvent = { + id: "product-1", + pubkey: "seller-pubkey", + created_at: 1710000000, + kind: 30402, + content: "", + sig: "sig", + tags: [], + }; + + it("parses shared tags once and exposes alias fields", () => { + const event: NostrEvent = { + ...baseEvent, + tags: [ + ["title", "Tea"], + ["summary", "Organic green tea"], + ["price", "12", "USD"], + ["required_customer_info", "Phone number"], + ["restrictions", "US only"], + ], + }; + + const canonical = parseCanonicalProductEvent(event); + + expect(canonical.title).toBe("Tea"); + expect(canonical.summary).toBe("Organic green tea"); + expect(canonical.price).toBe(12); + expect(canonical.currency).toBe("USD"); + expect(canonical.requiredCustomerInfo).toBe("Phone number"); + expect(canonical.required).toBe("Phone number"); + expect(canonical.restrictions).toBe("US only"); + }); + + it("uses last valid shipping tag", () => { + const event: NostrEvent = { + ...baseEvent, + tags: [ + ["shipping", "5", "USD"], + ["shipping", "Added Cost", "7", "USD"], + ], + }; + + const canonical = parseCanonicalProductEvent(event); + + expect(canonical.shippingType).toBe("Added Cost"); + expect(canonical.shippingCost).toBe(7); + }); + + it("builds MCP-safe variant and subscription blocks", () => { + const event: NostrEvent = { + ...baseEvent, + tags: [ + ["size", "M", "5"], + ["volume", "250g", "18"], + ["weight", "1 lb", "30"], + ["bulk", "10", "99"], + ["subscription", "true"], + ["subscription_discount", "15"], + ["subscription_frequency", "weekly", "monthly"], + ], + }; + + const canonical = parseCanonicalProductEvent(event); + const mcpProduct = toMcpProductData(canonical); + + expect(mcpProduct.sizes).toEqual([{ size: "M", quantity: 5 }]); + expect(mcpProduct.volumes).toEqual([{ volume: "250g", price: 18 }]); + expect(mcpProduct.weights).toEqual([{ weight: "1 lb", price: 30 }]); + expect(mcpProduct.bulk).toEqual([{ units: 10, price: 99 }]); + expect(mcpProduct.subscription).toEqual({ + enabled: true, + discount: 15, + frequencies: ["weekly", "monthly"], + }); + }); +}); diff --git a/utils/parsers/__tests__/product-parser-functions.test.ts b/utils/parsers/__tests__/product-parser-functions.test.ts index 6087e7801..c0c611044 100644 --- a/utils/parsers/__tests__/product-parser-functions.test.ts +++ b/utils/parsers/__tests__/product-parser-functions.test.ts @@ -277,4 +277,14 @@ describe("parseTags", () => { expect(result.contentWarning).toBeFalsy(); }); + + it("should map required_customer_info to required for UI consumers", () => { + const event = { + ...baseEvent, + tags: [["required_customer_info", "Phone number"]], + }; + const result = parseTags(event)!; + + expect(result.required).toBe("Phone number"); + }); }); diff --git a/utils/parsers/product-event/base-parser.ts b/utils/parsers/product-event/base-parser.ts new file mode 100644 index 000000000..61ce687c8 --- /dev/null +++ b/utils/parsers/product-event/base-parser.ts @@ -0,0 +1,249 @@ +import { ShippingOptionsType } from "@/utils/STATIC-VARIABLES"; +import { parseShippingTag } from "@/utils/parsers/product-tag-helpers"; +import { NostrEvent } from "@/utils/types/types"; + +export type CanonicalSizeOption = { + size: string; + quantity?: number; +}; + +export type CanonicalVolumeOption = { + volume: string; + price?: number; +}; + +export type CanonicalWeightOption = { + weight: string; + price?: number; +}; + +export type CanonicalBulkTier = { + units: number; + price: number; +}; + +export type CanonicalProductData = { + id: string; + pubkey: string; + createdAt: number; + d?: string; + title?: string; + summary?: string; + publishedAt?: string; + images?: string[]; + categories?: string[]; + location?: string; + price: number; + currency?: string; + shippingType?: ShippingOptionsType; + shippingCost?: number; + contentWarning: boolean; + quantity?: number; + sizes?: CanonicalSizeOption[]; + volumes?: CanonicalVolumeOption[]; + weights?: CanonicalWeightOption[]; + bulk?: CanonicalBulkTier[]; + condition?: string; + status?: string; + required?: string; + requiredCustomerInfo?: string; + restrictions?: string; + pickupLocations?: string[]; + expiration?: number; + subscriptionEnabled: boolean; + subscriptionDiscount?: number; + subscriptionFrequencies?: string[]; +}; + +function parseNumber(value?: string): number | undefined { + if (value == null || !String(value).trim()) { + return; + } + + const parsed = Number(value); + if (!Number.isFinite(parsed)) { + return; + } + + return parsed; +} + +export function parseCanonicalProductEvent( + productEvent: NostrEvent +): CanonicalProductData { + const parsedData: CanonicalProductData = { + id: productEvent.id, + pubkey: productEvent.pubkey, + createdAt: productEvent.created_at, + price: 0, + contentWarning: false, + subscriptionEnabled: false, + }; + + const tags = productEvent.tags || []; + + tags.forEach((tag) => { + const [key, ...values] = tag; + + switch (key) { + case "title": + parsedData.title = values[0] || undefined; + break; + case "summary": + parsedData.summary = values[0] || undefined; + break; + case "published_at": + parsedData.publishedAt = values[0] || undefined; + break; + case "image": + if (values[0]) { + if (!parsedData.images) parsedData.images = []; + parsedData.images.push(values[0]); + } + break; + case "t": + if (values[0]) { + if (!parsedData.categories) parsedData.categories = []; + parsedData.categories.push(values[0]); + } + break; + case "location": + parsedData.location = values[0] || undefined; + break; + case "price": { + const amount = parseNumber(values[0]); + parsedData.price = amount ?? 0; + parsedData.currency = values[1] || undefined; + break; + } + case "shipping": { + const parsedShipping = parseShippingTag(tag); + if (parsedShipping) { + parsedData.shippingType = parsedShipping.shippingType; + parsedData.shippingCost = parsedShipping.shippingCost; + } + break; + } + case "d": + parsedData.d = values[0]; + break; + case "content-warning": + parsedData.contentWarning = true; + break; + case "L": + if (values[0] === "content-warning") { + parsedData.contentWarning = true; + } + break; + case "l": + if (values[1] === "content-warning") { + parsedData.contentWarning = true; + } + break; + case "quantity": { + const quantity = parseNumber(values[0]); + if (quantity !== undefined) { + parsedData.quantity = quantity; + } + break; + } + case "size": { + const size = values[0]; + if (!size) break; + if (!parsedData.sizes) parsedData.sizes = []; + parsedData.sizes.push({ + size, + quantity: parseNumber(values[1]), + }); + break; + } + case "volume": { + const volume = values[0]; + if (!volume) break; + if (!parsedData.volumes) parsedData.volumes = []; + parsedData.volumes.push({ + volume, + price: parseNumber(values[1]), + }); + break; + } + case "weight": { + const weight = values[0]; + if (!weight) break; + if (!parsedData.weights) parsedData.weights = []; + parsedData.weights.push({ + weight, + price: parseNumber(values[1]), + }); + break; + } + case "bulk": { + const units = parseNumber(values[0]); + const price = parseNumber(values[1]); + if (units !== undefined && price !== undefined) { + if (!parsedData.bulk) parsedData.bulk = []; + parsedData.bulk.push({ units, price }); + } + break; + } + case "condition": + parsedData.condition = values[0]; + break; + case "status": + parsedData.status = values[0]; + break; + case "required": + parsedData.required = values[0]; + break; + case "required_customer_info": + parsedData.requiredCustomerInfo = values[0]; + break; + case "restrictions": + parsedData.restrictions = values[0]; + break; + case "pickup_location": + if (values[0]) { + if (!parsedData.pickupLocations) parsedData.pickupLocations = []; + parsedData.pickupLocations.push(values[0]); + } + break; + case "valid_until": { + const expiration = parseNumber(values[0]); + if (expiration !== undefined) { + parsedData.expiration = expiration; + } + break; + } + case "subscription": + parsedData.subscriptionEnabled = values[0] === "true"; + break; + case "subscription_discount": { + const discount = parseNumber(values[0]); + if (discount !== undefined) { + parsedData.subscriptionDiscount = discount; + } + break; + } + case "subscription_frequency": { + const frequencies = values.filter(Boolean); + if (frequencies.length > 0) { + if (!parsedData.subscriptionFrequencies) + parsedData.subscriptionFrequencies = []; + parsedData.subscriptionFrequencies = frequencies; + } + break; + } + default: + break; + } + }); + + if (!parsedData.required && parsedData.requiredCustomerInfo) { + parsedData.required = parsedData.requiredCustomerInfo; + } + if (!parsedData.requiredCustomerInfo && parsedData.required) { + parsedData.requiredCustomerInfo = parsedData.required; + } + + return parsedData; +} diff --git a/utils/parsers/product-event/mcp-adapter.ts b/utils/parsers/product-event/mcp-adapter.ts new file mode 100644 index 000000000..01f9d6dea --- /dev/null +++ b/utils/parsers/product-event/mcp-adapter.ts @@ -0,0 +1,91 @@ +import { CanonicalProductData } from "./base-parser"; + +export type McpProductData = { + id: string; + pubkey: string; + d?: string; + title: string; + summary: string; + images: string[]; + categories: string[]; + location: string; + price: number; + currency: string; + shippingType?: string; + shippingCost?: number; + quantity?: number; + condition?: string; + status?: string; + sizes?: Array<{ size: string; quantity?: number }>; + volumes?: Array<{ volume: string; price?: number }>; + weights?: Array<{ weight: string; price?: number }>; + bulk?: Array<{ units: number; price: number }>; + pickupLocations?: string[]; + requiredCustomerInfo?: string; + createdAt: number; + publishedAt?: string; + validUntil?: number; + contentWarning?: boolean; + restrictions?: string; + subscription: { + enabled: boolean; + discount?: number; + frequencies: string[]; + }; +}; + +export function toMcpProductData( + canonical: CanonicalProductData +): McpProductData { + const { + expiration, + required, + requiredCustomerInfo, + subscriptionEnabled, + subscriptionDiscount, + subscriptionFrequencies, + ...mcpCompatibleFields + } = canonical; + + return { + ...mcpCompatibleFields, + title: canonical.title ?? "", + summary: canonical.summary ?? "", + images: [...(canonical.images ?? [])], + categories: [...(canonical.categories ?? [])], + location: canonical.location ?? "", + currency: canonical.currency ?? "", + sizes: + canonical.sizes && canonical.sizes.length > 0 + ? canonical.sizes.map((s) => ({ size: s.size, quantity: s.quantity })) + : undefined, + volumes: + canonical.volumes && canonical.volumes.length > 0 + ? canonical.volumes.map((v) => ({ volume: v.volume, price: v.price })) + : undefined, + weights: + canonical.weights && canonical.weights.length > 0 + ? canonical.weights.map((w) => ({ weight: w.weight, price: w.price })) + : undefined, + bulk: + canonical.bulk && canonical.bulk.length > 0 + ? canonical.bulk.map((tier) => ({ + units: tier.units, + price: tier.price, + })) + : undefined, + pickupLocations: + canonical.pickupLocations && canonical.pickupLocations.length > 0 + ? [...canonical.pickupLocations] + : undefined, + requiredCustomerInfo: requiredCustomerInfo ?? required, + publishedAt: canonical.publishedAt || undefined, + validUntil: expiration, + contentWarning: canonical.contentWarning || undefined, + subscription: { + enabled: subscriptionEnabled, + discount: subscriptionDiscount, + frequencies: [...(subscriptionFrequencies ?? [])], + }, + }; +} diff --git a/utils/parsers/product-event/ui-adapter.ts b/utils/parsers/product-event/ui-adapter.ts new file mode 100644 index 000000000..4ac23ae92 --- /dev/null +++ b/utils/parsers/product-event/ui-adapter.ts @@ -0,0 +1,78 @@ +import { calculateTotalCost } from "@/components/utility-components/display-monetary-info"; +import { ProductData } from "@/utils/parsers/product-types"; +import { NostrEvent } from "@/utils/types/types"; + +import { CanonicalProductData } from "./base-parser"; + +export function toUiProductData( + canonical: CanonicalProductData, + rawEvent: NostrEvent +): ProductData { + const { + sizes, + volumes, + weights, + bulk, + requiredCustomerInfo: _requiredCustomerInfo, + subscriptionEnabled: _subscriptionEnabled, + subscriptionDiscount: _subscriptionDiscount, + subscriptionFrequencies: _subscriptionFrequencies, + ...uiCompatibleFields + } = canonical; + + const parsedData: ProductData = { + ...uiCompatibleFields, + title: canonical.title ?? "", + summary: canonical.summary ?? "", + publishedAt: canonical.publishedAt ?? "", + images: [...(canonical.images ?? [])], + categories: [...(canonical.categories ?? [])], + location: canonical.location ?? "", + currency: canonical.currency ?? "", + totalCost: 0, + contentWarning: canonical.contentWarning || undefined, + pickupLocations: + canonical.pickupLocations && canonical.pickupLocations.length > 0 + ? [...canonical.pickupLocations] + : undefined, + rawEvent, + }; + + if (sizes && sizes.length > 0) { + parsedData.sizes = sizes.map((s) => s.size); + parsedData.sizeQuantities = new Map(); + sizes.forEach((s) => { + parsedData.sizeQuantities?.set(s.size, Number(s.quantity)); + }); + } + + if (volumes && volumes.length > 0) { + parsedData.volumes = volumes.map((v) => v.volume); + parsedData.volumePrices = new Map(); + volumes.forEach((v) => { + if (v.price !== undefined) { + parsedData.volumePrices?.set(v.volume, v.price); + } + }); + } + + if (weights && weights.length > 0) { + parsedData.weights = weights.map((w) => w.weight); + parsedData.weightPrices = new Map(); + weights.forEach((w) => { + if (w.price !== undefined) { + parsedData.weightPrices?.set(w.weight, w.price); + } + }); + } + + if (bulk && bulk.length > 0) { + parsedData.bulkPrices = new Map(); + bulk.forEach((tier) => { + parsedData.bulkPrices?.set(tier.units, tier.price); + }); + } + + parsedData.totalCost = calculateTotalCost(parsedData); + return parsedData; +} diff --git a/utils/parsers/product-parser-functions.ts b/utils/parsers/product-parser-functions.ts index d308e3f0a..2fd6f32dc 100644 --- a/utils/parsers/product-parser-functions.ts +++ b/utils/parsers/product-parser-functions.ts @@ -1,193 +1,15 @@ -import { ShippingOptionsType } from "@/utils/STATIC-VARIABLES"; -import { calculateTotalCost } from "@/components/utility-components/display-monetary-info"; -import { parseShippingTag } from "@/utils/parsers/product-tag-helpers"; +import { parseCanonicalProductEvent } from "@/utils/parsers/product-event/base-parser"; +import { toUiProductData } from "@/utils/parsers/product-event/ui-adapter"; +import { ProductData } from "@/utils/parsers/product-types"; import { NostrEvent } from "@/utils/types/types"; -export type ProductData = { - id: string; - pubkey: string; - createdAt: number; - title: string; - summary: string; - publishedAt: string; - images: string[]; - categories: string[]; - location: string; - price: number; - currency: string; - shippingType?: ShippingOptionsType; - shippingCost?: number; - totalCost: number; - d?: string; - contentWarning?: boolean; - quantity?: number; - sizes?: string[]; - sizeQuantities?: Map; - volumes?: string[]; - volumePrices?: Map; - weights?: string[]; - weightPrices?: Map; - condition?: string; - status?: string; - selectedSize?: string; - selectedQuantity?: number; - selectedVolume?: string; - volumePrice?: number; - selectedWeight?: string; - weightPrice?: number; - bulkPrices?: Map; - selectedBulkOption?: number; - bulkPrice?: number; - required?: string; - restrictions?: string; - pickupLocations?: string[]; - expiration?: number; - rawEvent?: NostrEvent; -}; +export type { ProductData }; export const parseTags = (productEvent: NostrEvent) => { - const parsedData: ProductData = { - id: "", - pubkey: "", - createdAt: 0, - title: "", - summary: "", - publishedAt: "", - images: [], - categories: [], - location: "", - price: 0, - currency: "", - totalCost: 0, - rawEvent: productEvent, - }; - parsedData.pubkey = productEvent.pubkey; - parsedData.id = productEvent.id; - parsedData.createdAt = productEvent.created_at; - const tags = productEvent.tags; - if (tags === undefined) return; - tags.forEach((tag) => { - const [key, ...values] = tag; - switch (key) { - case "title": - parsedData.title = values[0]!; - break; - case "summary": - parsedData.summary = values[0]!; - break; - case "published_at": - parsedData.publishedAt = values[0]!; - break; - case "image": - if (parsedData.images === undefined) parsedData.images = []; - parsedData.images.push(values[0]!); - break; - case "t": - if (parsedData.categories === undefined) parsedData.categories = []; - parsedData.categories.push(values[0]!); - break; - case "location": - parsedData.location = values[0]!; - break; - case "price": - const [amount, currency] = values; - parsedData.price = Number(amount); - parsedData.currency = currency!; - break; - case "shipping": - const parsedShipping = parseShippingTag(tag); - if (parsedShipping) { - parsedData.shippingType = parsedShipping.shippingType; - parsedData.shippingCost = parsedShipping.shippingCost; - } - break; - case "d": - parsedData.d = values[0]; - break; - case "content-warning": - parsedData.contentWarning = true; - break; - case "L": - const LValue = values[0]; - if (LValue === "content-warning") { - parsedData.contentWarning = true; - } - break; - case "l": - const lValue = values[1]; - if (lValue === "content-warning") { - parsedData.contentWarning = true; - } - break; - case "quantity": - parsedData.quantity = Number(values[0]); - break; - case "size": - const [size, quantity] = values; - if (parsedData.sizes === undefined) parsedData.sizes = []; - parsedData.sizes?.push(size!); - if (parsedData.sizeQuantities === undefined) - parsedData.sizeQuantities = new Map(); - parsedData.sizeQuantities.set(size!, Number(quantity)); - break; - case "volume": - if (!parsedData.volumes) { - parsedData.volumes = []; - parsedData.volumePrices = new Map(); - } - if (values[0]) { - parsedData.volumes.push(values[0]); - if (values[1]) { - parsedData.volumePrices!.set(values[0], parseFloat(values[1])); - } - } - break; - case "weight": - if (!parsedData.weights) { - parsedData.weights = []; - parsedData.weightPrices = new Map(); - } - if (values[0]) { - parsedData.weights.push(values[0]); - if (values[1]) { - parsedData.weightPrices!.set(values[0], parseFloat(values[1])); - } - } - break; - case "bulk": - if (!parsedData.bulkPrices) { - parsedData.bulkPrices = new Map(); - } - if (values[0] && values[1]) { - parsedData.bulkPrices.set(parseInt(values[0]), parseFloat(values[1])); - } - break; - case "condition": - parsedData.condition = values[0]; - break; - case "status": - parsedData.status = values[0]; - break; - case "required": - parsedData.required = values[0]; - break; - case "restrictions": - parsedData.restrictions = values[0]; - break; - case "pickup_location": - if (parsedData.pickupLocations === undefined) - parsedData.pickupLocations = []; - parsedData.pickupLocations.push(values[0]!); - break; - case "valid_until": - parsedData.expiration = Number(values[0]); - break; - default: - return; - } - }); - parsedData.totalCost = calculateTotalCost(parsedData); - return parsedData; + if (productEvent.tags === undefined) return; + + const canonical = parseCanonicalProductEvent(productEvent); + return toUiProductData(canonical, productEvent); }; export default parseTags; diff --git a/utils/parsers/product-types.ts b/utils/parsers/product-types.ts new file mode 100644 index 000000000..eef4e12cc --- /dev/null +++ b/utils/parsers/product-types.ts @@ -0,0 +1,44 @@ +import { ShippingOptionsType } from "@/utils/STATIC-VARIABLES"; +import { NostrEvent } from "@/utils/types/types"; + +export type ProductData = { + id: string; + pubkey: string; + createdAt: number; + title: string; + summary: string; + publishedAt: string; + images: string[]; + categories: string[]; + location: string; + price: number; + currency: string; + shippingType?: ShippingOptionsType; + shippingCost?: number; + totalCost: number; + d?: string; + contentWarning?: boolean; + quantity?: number; + sizes?: string[]; + sizeQuantities?: Map; + volumes?: string[]; + volumePrices?: Map; + weights?: string[]; + weightPrices?: Map; + condition?: string; + status?: string; + selectedSize?: string; + selectedQuantity?: number; + selectedVolume?: string; + volumePrice?: number; + selectedWeight?: string; + weightPrice?: number; + bulkPrices?: Map; + selectedBulkOption?: number; + bulkPrice?: number; + required?: string; + restrictions?: string; + pickupLocations?: string[]; + expiration?: number; + rawEvent?: NostrEvent; +};