From 25d17b481d59bcc86839132b373f1fa39918867d Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 26 Sep 2025 17:29:06 +0200 Subject: [PATCH] refactor: consolidate Product types into single source of truth - Replace dual Product interfaces with unified domain model in types/market.ts - Rename API response type to ProductApiResponse for clarity - Add mapApiResponseToProduct() function for clean API-to-domain conversion - Update MerchantStore.vue to use mapping function instead of manual property assignment - Fix CreateProductDialog.vue to properly handle type conversions for API calls - Resolve TypeScript readonly type conflicts using type assertions instead of array spreading - Add missing reactive references (searchQuery) and remove unused imports - Prevent Vue 3 recursion issues by maintaining original reactive object references This eliminates confusion between API response structure and application domain model, following single source of truth principle for better maintainability and type safety. --- .../market/components/CreateProductDialog.vue | 17 +++-- .../market/components/MerchantStore.vue | 36 ++++------ src/modules/market/services/nostrmarketAPI.ts | 21 +++--- src/modules/market/types/market.ts | 66 +++++++++++++++++++ 4 files changed, 100 insertions(+), 40 deletions(-) diff --git a/src/modules/market/components/CreateProductDialog.vue b/src/modules/market/components/CreateProductDialog.vue index 636cff7..93d6042 100644 --- a/src/modules/market/components/CreateProductDialog.vue +++ b/src/modules/market/components/CreateProductDialog.vue @@ -219,7 +219,8 @@ import { FormMessage, } from '@/components/ui/form' import { Package } from 'lucide-vue-next' -import type { NostrmarketAPI, Stall, Product, CreateProductRequest } from '../services/nostrmarketAPI' +import type { NostrmarketAPI, Stall, CreateProductRequest } from '../services/nostrmarketAPI' +import type { Product } from '../types/market' import { auth } from '@/composables/useAuthService' import { useToast } from '@/core/composables/useToast' import { injectService, SERVICE_TOKENS } from '@/core/di-container' @@ -326,9 +327,9 @@ const updateProduct = async (formData: any) => { createError.value = null try { - const productData: Product = { - id: props.product.id, - stall_id: props.product.stall_id, + const productData = { + id: props.product?.id, + stall_id: props.product?.stall_id || props.stall?.id || '', name, categories: categories || [], images: images || [], @@ -338,11 +339,13 @@ const updateProduct = async (formData: any) => { pending: false, config: { description: description || '', - currency: props.stall?.currency || props.product.config.currency, + currency: props.stall?.currency || props.product?.config?.currency || 'sats', use_autoreply, autoreply_message: use_autoreply ? autoreply_message || '' : '', - shipping: props.product.config.shipping || [] - } + shipping: props.product?.config?.shipping || [] + }, + event_id: props.product?.nostrEventId, + event_created_at: props.product?.createdAt } const adminKey = paymentService.getPreferredWalletAdminKey() diff --git a/src/modules/market/components/MerchantStore.vue b/src/modules/market/components/MerchantStore.vue index f4ea030..6e4d84d 100644 --- a/src/modules/market/components/MerchantStore.vue +++ b/src/modules/market/components/MerchantStore.vue @@ -258,7 +258,7 @@

{{ product.name }}

-

+

{{ product.config.description }}

@@ -266,7 +266,7 @@
- {{ product.price }} {{ product.config.currency || activeStall?.currency || 'sat' }} + {{ product.price }} {{ product.config?.currency || activeStall?.currency || 'sat' }}
Qty: {{ product.quantity }} @@ -342,7 +342,9 @@ import { AlertCircle, User } from 'lucide-vue-next' -import type { NostrmarketAPI, Merchant, Stall, Product } from '../services/nostrmarketAPI' +import type { NostrmarketAPI, Merchant, Stall } from '../services/nostrmarketAPI' +import type { Product } from '../types/market' +import { mapApiResponseToProduct } from '../types/market' import { auth } from '@/composables/useAuthService' import { useToast } from '@/core/composables/useToast' import { injectService, SERVICE_TOKENS } from '@/core/di-container' @@ -516,26 +518,14 @@ const loadStallProducts = async () => { inkey, activeStall.value.id! ) - // Enrich products with stall name and missing properties to match Product interface - const enrichedProducts = (products || []).map(product => ({ - id: product.id || `${product.stall_id}-${Date.now()}`, // Ensure id is always string - stall_id: product.stall_id, - stallName: activeStall.value?.name || 'Unknown Stall', - name: product.name, - description: product.config?.description || '', - price: product.price, - currency: activeStall.value?.currency || 'sats', // Use stall currency - quantity: product.quantity, - images: product.images, - categories: product.categories, - createdAt: product.event_created_at || Date.now(), - updatedAt: Date.now(), - nostrEventId: product.event_id, - // API-specific properties that are expected by the template - active: product.active ?? true, - pending: product.pending ?? false, - config: product.config || { currency: activeStall.value?.currency || 'sats' } - })) + // Convert API responses to domain models using clean mapping function + const enrichedProducts = (products || []).map(product => + mapApiResponseToProduct( + product, + activeStall.value?.name || 'Unknown Stall', + activeStall.value?.currency || 'sats' + ) + ) stallProducts.value = enrichedProducts // Only add active products to the market store so they appear in the main market diff --git a/src/modules/market/services/nostrmarketAPI.ts b/src/modules/market/services/nostrmarketAPI.ts index ea1ba55..77e56bf 100644 --- a/src/modules/market/services/nostrmarketAPI.ts +++ b/src/modules/market/services/nostrmarketAPI.ts @@ -68,7 +68,8 @@ export interface ProductConfig { shipping: ProductShippingCost[] } -export interface Product { +// API Response Types - Raw data from LNbits API +export interface ProductApiResponse { id?: string stall_id: string name: string @@ -358,8 +359,8 @@ export class NostrmarketAPI extends BaseService { /** * Get products for a stall */ - async getProducts(walletInkey: string, stallId: string, pending: boolean = false): Promise { - const products = await this.request( + async getProducts(walletInkey: string, stallId: string, pending: boolean = false): Promise { + const products = await this.request( `/api/v1/stall/product/${stallId}?pending=${pending}`, walletInkey, { method: 'GET' } @@ -380,8 +381,8 @@ export class NostrmarketAPI extends BaseService { async createProduct( walletAdminkey: string, productData: CreateProductRequest - ): Promise { - const product = await this.request( + ): Promise { + const product = await this.request( '/api/v1/product', walletAdminkey, { @@ -405,9 +406,9 @@ export class NostrmarketAPI extends BaseService { async updateProduct( walletAdminkey: string, productId: string, - productData: Product - ): Promise { - const product = await this.request( + productData: ProductApiResponse + ): Promise { + const product = await this.request( `/api/v1/product/${productId}`, walletAdminkey, { @@ -427,9 +428,9 @@ export class NostrmarketAPI extends BaseService { /** * Get a single product by ID */ - async getProduct(walletInkey: string, productId: string): Promise { + async getProduct(walletInkey: string, productId: string): Promise { try { - const product = await this.request( + const product = await this.request( `/api/v1/product/${productId}`, walletInkey, { method: 'GET' } diff --git a/src/modules/market/types/market.ts b/src/modules/market/types/market.ts index 253041e..5bfb290 100644 --- a/src/modules/market/types/market.ts +++ b/src/modules/market/types/market.ts @@ -26,6 +26,7 @@ export interface Stall { nostrEventId?: string } +// Domain Model - Single source of truth for Product export interface Product { id: string stall_id: string @@ -46,6 +47,71 @@ export interface Product { config?: { currency?: string, [key: string]: any } } +// Type aliases for API responses - imported from services +export type { ProductApiResponse, Stall as StallApiResponse } from '../services/nostrmarketAPI' + +// Mapping function to convert API response to domain model +export function mapApiResponseToProduct( + apiProduct: import('../services/nostrmarketAPI').ProductApiResponse, + stallName: string, + stallCurrency: string = 'sats' +): Product { + return { + id: apiProduct.id || `${apiProduct.stall_id}-${Date.now()}`, + stall_id: apiProduct.stall_id, + stallName, + name: apiProduct.name, + description: apiProduct.config?.description || '', + price: apiProduct.price, + currency: stallCurrency, + quantity: apiProduct.quantity, + images: apiProduct.images, + categories: apiProduct.categories, + createdAt: apiProduct.event_created_at || Date.now(), + updatedAt: Date.now(), + nostrEventId: apiProduct.event_id, + active: apiProduct.active, + pending: apiProduct.pending, + config: apiProduct.config + } +} + +// Mapping function to convert API response to domain model for Stall +export function mapApiResponseToStall( + apiStall: import('../services/nostrmarketAPI').Stall +): Stall { + return { + id: apiStall.id, + pubkey: '', // API Stall doesn't have pubkey, need to set from merchant context + name: apiStall.name, + description: apiStall.config?.description, + logo: apiStall.config?.image_url, + categories: [], // API Stall doesn't have categories + shipping: apiStall.shipping_zones?.map(zone => ({ + id: zone.id, + name: zone.name, + cost: zone.cost, + currency: apiStall.currency, + countries: zone.countries, + description: `${zone.name} shipping`, + estimatedDays: undefined, + requiresPhysicalShipping: true + })), + shipping_zones: apiStall.shipping_zones?.map(zone => ({ + id: zone.id, + name: zone.name, + cost: zone.cost, + currency: apiStall.currency, + countries: zone.countries, + description: `${zone.name} shipping`, + estimatedDays: undefined, + requiresPhysicalShipping: true + })), + currency: apiStall.currency, + nostrEventId: apiStall.event_id + } +} + export interface Order { id: string cartId: string