diff --git a/CLAUDE.md b/CLAUDE.md index fb2d0ad..6fd5d02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,6 +33,12 @@ The application uses a plugin-based modular architecture with dependency injecti - **Events Module** (`src/modules/events/`) - Event ticketing with Lightning payments - **Market Module** (`src/modules/market/`) - Nostr marketplace functionality +**IMPORTANT - Market Event Publishing Strategy:** +- **LNbits "nostrmarket" extension handles ALL market event publishing** (merchants, stalls, products) to Nostr relays +- **Web-app does NOT publish** merchant/stall/product events - only processes incoming events from relays +- **Exception: Checkout/Order events** - Web-app publishes order events directly to Nostr during checkout process +- This division ensures consistency and prevents duplicate publishing while allowing real-time order placement + **Module Configuration:** - Modules are configured in `src/app.config.ts` - Each module can be enabled/disabled and configured independently diff --git a/package-lock.json b/package-lock.json index 2250669..aa24fb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "aio-shadcn-vite", "version": "0.0.0", "dependencies": { - "@tanstack/vue-table": "^8.21.2", + "@tanstack/vue-table": "^8.21.3", "@vee-validate/zod": "^4.15.1", "@vueuse/components": "^12.5.0", "@vueuse/core": "^12.8.2", @@ -4950,9 +4950,9 @@ } }, "node_modules/@tanstack/table-core": { - "version": "8.21.2", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz", - "integrity": "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", "license": "MIT", "engines": { "node": ">=12" @@ -4973,12 +4973,12 @@ } }, "node_modules/@tanstack/vue-table": { - "version": "8.21.2", - "resolved": "https://registry.npmjs.org/@tanstack/vue-table/-/vue-table-8.21.2.tgz", - "integrity": "sha512-KBgOWxha/x4m1EdhVWxOpqHb661UjqAxzPcmXR3QiA7aShZ547x19Gw0UJX9we+m+tVcPuLRZ61JsYW47QZFfQ==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/vue-table/-/vue-table-8.21.3.tgz", + "integrity": "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==", "license": "MIT", "dependencies": { - "@tanstack/table-core": "8.21.2" + "@tanstack/table-core": "8.21.3" }, "engines": { "node": ">=12" diff --git a/package.json b/package.json index 34de2b4..304528e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "make": "electron-forge make" }, "dependencies": { - "@tanstack/vue-table": "^8.21.2", + "@tanstack/vue-table": "^8.21.3", "@vee-validate/zod": "^4.15.1", "@vueuse/components": "^12.5.0", "@vueuse/core": "^12.8.2", diff --git a/src/components/ui/table/Table.vue b/src/components/ui/table/Table.vue new file mode 100644 index 0000000..60ab055 --- /dev/null +++ b/src/components/ui/table/Table.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/components/ui/table/TableBody.vue b/src/components/ui/table/TableBody.vue new file mode 100644 index 0000000..58d1ba6 --- /dev/null +++ b/src/components/ui/table/TableBody.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/ui/table/TableCaption.vue b/src/components/ui/table/TableCaption.vue new file mode 100644 index 0000000..baaa177 --- /dev/null +++ b/src/components/ui/table/TableCaption.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/ui/table/TableCell.vue b/src/components/ui/table/TableCell.vue new file mode 100644 index 0000000..d42cf26 --- /dev/null +++ b/src/components/ui/table/TableCell.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/ui/table/TableEmpty.vue b/src/components/ui/table/TableEmpty.vue new file mode 100644 index 0000000..c454dab --- /dev/null +++ b/src/components/ui/table/TableEmpty.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/components/ui/table/TableFooter.vue b/src/components/ui/table/TableFooter.vue new file mode 100644 index 0000000..c3dc6b0 --- /dev/null +++ b/src/components/ui/table/TableFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/ui/table/TableHead.vue b/src/components/ui/table/TableHead.vue new file mode 100644 index 0000000..51ceec2 --- /dev/null +++ b/src/components/ui/table/TableHead.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/ui/table/TableHeader.vue b/src/components/ui/table/TableHeader.vue new file mode 100644 index 0000000..4977b71 --- /dev/null +++ b/src/components/ui/table/TableHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/ui/table/TableRow.vue b/src/components/ui/table/TableRow.vue new file mode 100644 index 0000000..fd363c0 --- /dev/null +++ b/src/components/ui/table/TableRow.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/ui/table/index.ts b/src/components/ui/table/index.ts new file mode 100644 index 0000000..3be308b --- /dev/null +++ b/src/components/ui/table/index.ts @@ -0,0 +1,9 @@ +export { default as Table } from "./Table.vue" +export { default as TableBody } from "./TableBody.vue" +export { default as TableCaption } from "./TableCaption.vue" +export { default as TableCell } from "./TableCell.vue" +export { default as TableEmpty } from "./TableEmpty.vue" +export { default as TableFooter } from "./TableFooter.vue" +export { default as TableHead } from "./TableHead.vue" +export { default as TableHeader } from "./TableHeader.vue" +export { default as TableRow } from "./TableRow.vue" diff --git a/src/components/ui/table/utils.ts b/src/components/ui/table/utils.ts new file mode 100644 index 0000000..3d4fd12 --- /dev/null +++ b/src/components/ui/table/utils.ts @@ -0,0 +1,10 @@ +import type { Updater } from "@tanstack/vue-table" + +import type { Ref } from "vue" +import { isFunction } from "@tanstack/vue-table" + +export function valueUpdater(updaterOrValue: Updater, ref: Ref) { + ref.value = isFunction(updaterOrValue) + ? updaterOrValue(ref.value) + : updaterOrValue +} diff --git a/src/modules/market/components/CreateProductDialog.vue b/src/modules/market/components/CreateProductDialog.vue index 3232804..dd5b505 100644 --- a/src/modules/market/components/CreateProductDialog.vue +++ b/src/modules/market/components/CreateProductDialog.vue @@ -455,6 +455,17 @@ const createProduct = async (formData: any) => { throw new Error('No wallet admin key available') } + // Debug: Log what we're sending + console.log('🛒 CreateProductDialog: About to create product with categories:', { + name: productData.name, + categories: productData.categories, + categoriesType: typeof productData.categories, + categoriesLength: productData.categories?.length, + formCategories: categories, + formData: formData, + fullProductData: productData + }) + const newProduct = await nostrmarketAPI.createProduct( adminKey, productData diff --git a/src/modules/market/components/DashboardOverview.vue b/src/modules/market/components/DashboardOverview.vue index bfe1b73..103c07a 100644 --- a/src/modules/market/components/DashboardOverview.vue +++ b/src/modules/market/components/DashboardOverview.vue @@ -215,69 +215,107 @@ diff --git a/src/modules/market/components/DeleteConfirmDialog.vue b/src/modules/market/components/DeleteConfirmDialog.vue new file mode 100644 index 0000000..7bc2065 --- /dev/null +++ b/src/modules/market/components/DeleteConfirmDialog.vue @@ -0,0 +1,85 @@ + + + \ No newline at end of file diff --git a/src/modules/market/components/MerchantOrders.vue b/src/modules/market/components/MerchantOrders.vue new file mode 100644 index 0000000..f4428a8 --- /dev/null +++ b/src/modules/market/components/MerchantOrders.vue @@ -0,0 +1,744 @@ + + + \ No newline at end of file diff --git a/src/modules/market/components/MerchantStore.vue b/src/modules/market/components/MerchantStore.vue index 2ded586..50da407 100644 --- a/src/modules/market/components/MerchantStore.vue +++ b/src/modules/market/components/MerchantStore.vue @@ -207,15 +207,23 @@ - +
-
-

Products

- -
+ + + Products + Orders + + + + +
+

Products

+ +
@@ -276,6 +284,19 @@ {{ product.active ? 'Active' : 'Inactive' }} + + +
+ + + +
@@ -292,18 +313,54 @@ -
- + +
+ + + + + + + + @@ -325,6 +382,16 @@ @created="onProductCreated" @updated="onProductUpdated" /> + + + @@ -334,15 +401,27 @@ import { useRouter } from 'vue-router' import { useMarketStore } from '@/modules/market/stores/market' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@/components/ui/tabs' import { Package, Store, DollarSign, Star, Plus, - User + User, + Trash2, + Send, + Edit, + CheckCircle, + Clock, + AlertCircle } from 'lucide-vue-next' -import type { NostrmarketAPI, Merchant, Stall } from '../services/nostrmarketAPI' +import type { NostrmarketAPI, Merchant, Stall, ProductApiResponse } from '../services/nostrmarketAPI' import type { Product } from '../types/market' import { mapApiResponseToProduct } from '../types/market' import { auth } from '@/composables/useAuthService' @@ -350,7 +429,9 @@ import { useToast } from '@/core/composables/useToast' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import CreateStoreDialog from './CreateStoreDialog.vue' import CreateProductDialog from './CreateProductDialog.vue' +import DeleteConfirmDialog from './DeleteConfirmDialog.vue' import StoreCard from './StoreCard.vue' +import MerchantOrders from './MerchantOrders.vue' const router = useRouter() const marketStore = useMarketStore() @@ -376,10 +457,25 @@ const activeStall = computed(() => const stallProducts = ref([]) const isLoadingProducts = ref(false) +// Product action state +const isDeletingProduct = ref(false) +const deletingProductId = ref(null) +const isResendingProduct = ref(false) +const resendingProductId = ref(null) + +// Nostr sync tracking +const pendingNostrConfirmation = ref>(new Map()) // productId -> timestamp +const confirmedOnNostr = ref>(new Set()) + +// Tab management +const activeTab = ref('products') + // Dialog state const showCreateStoreDialog = ref(false) const showCreateProductDialog = ref(false) +const showDeleteConfirmDialog = ref(false) const editingProduct = ref(null) +const productToDelete = ref(null) // Computed properties const userHasMerchantProfile = computed(() => { @@ -390,6 +486,17 @@ const userHasStalls = computed(() => { return userStalls.value.length > 0 }) +// Helper to get sync status for a product +const getProductSyncStatus = (productId: string) => { + if (confirmedOnNostr.value.has(productId)) { + return 'confirmed' + } + if (pendingNostrConfirmation.value.has(productId)) { + return 'pending' + } + return 'unknown' +} + const storeStats = computed(() => { const currentUserPubkey = auth.currentUser?.value?.pubkey if (!currentUserPubkey) { @@ -534,6 +641,9 @@ const loadStallProducts = async () => { .forEach(product => { marketStore.addProduct(product) }) + + // Initialize sync status for loaded products + initializeSyncStatus() } catch (error) { console.error('Failed to load products:', error) stallProducts.value = [] @@ -605,11 +715,248 @@ const editProduct = (product: Product) => { showCreateProductDialog.value = true } +const deleteProduct = (product: Product) => { + productToDelete.value = product + showDeleteConfirmDialog.value = true +} + +const confirmDeleteProduct = async () => { + if (!productToDelete.value) return + + const product = productToDelete.value + + try { + isDeletingProduct.value = true + deletingProductId.value = product.id + + const adminKey = paymentService.getPreferredWalletAdminKey() + if (!adminKey) { + throw new Error('No wallet admin key available') + } + + await nostrmarketAPI.deleteProduct(adminKey, product.id) + + // Remove from local state + stallProducts.value = stallProducts.value.filter(p => p.id !== product.id) + + showDeleteConfirmDialog.value = false + productToDelete.value = null + toast.success(`Product "${product.name}" deleted successfully!`) + } catch (error) { + console.error('Failed to delete product:', error) + toast.error('Failed to delete product. Please try again.') + } finally { + isDeletingProduct.value = false + deletingProductId.value = null + } +} + +const cancelDeleteProduct = () => { + showDeleteConfirmDialog.value = false + productToDelete.value = null +} + +const resendProduct = async (product: Product) => { + try { + isResendingProduct.value = true + resendingProductId.value = product.id + + const adminKey = paymentService.getPreferredWalletAdminKey() + if (!adminKey) { + throw new Error('No wallet admin key available') + } + + // Re-send by updating the product with its current data + // This will trigger LNbits to re-publish to Nostr + const productData: ProductApiResponse = { + id: product.id, + stall_id: product.stall_id, + name: product.name, + categories: product.categories || [], + images: product.images || [], + price: product.price, + quantity: product.quantity, + active: product.active ?? true, + pending: product.pending ?? false, + config: { + description: product.description || '', + currency: product.currency || 'sat', + use_autoreply: false, + autoreply_message: '', + shipping: [] + }, + event_id: product.nostrEventId, + event_created_at: product.createdAt + } + + await nostrmarketAPI.updateProduct(adminKey, product.id, productData) + + // Reset sync status - remove from confirmed and add to pending + confirmedOnNostr.value.delete(product.id) + pendingNostrConfirmation.value.set(product.id, Date.now()) + + console.log('🔄 Product re-sent - sync status reset to pending:', { + productId: product.id, + productName: product.name, + wasConfirmed: confirmedOnNostr.value.has(product.id), + nowPending: pendingNostrConfirmation.value.has(product.id) + }) + + toast.success(`Product "${product.name}" re-sent to LNbits for event publishing!`) + + // TODO: Consider adding a timeout to remove from pending if not confirmed within reasonable time + // (e.g., 30 seconds) to avoid keeping products in pending state indefinitely + } catch (error) { + console.error('Failed to re-send product:', error) + toast.error('Failed to re-send product. Please try again.') + } finally { + isResendingProduct.value = false + resendingProductId.value = null + } +} + const closeProductDialog = () => { showCreateProductDialog.value = false editingProduct.value = null } +// Watch for market store updates to detect confirmed products +watch(() => marketStore.products, (newProducts) => { + // Check if any pending products now appear in the market feed + for (const [productId, timestamp] of pendingNostrConfirmation.value.entries()) { + const foundProduct = newProducts.find(p => p.id === productId) + + if (foundProduct) { + // Find the corresponding local product to compare content + const localProduct = stallProducts.value.find(p => p.id === productId) + + if (localProduct) { + // Compare content to verify true sync + const localData = normalizeProductForComparison(localProduct) + const marketData = normalizeProductForComparison(foundProduct) + const localJson = JSON.stringify(localData) + const marketJson = JSON.stringify(marketData) + const isContentSynced = localJson === marketJson + + + if (isContentSynced) { + // Product content confirmed as synced on Nostr! + pendingNostrConfirmation.value.delete(productId) + confirmedOnNostr.value.add(productId) + + // Show confirmation toast + toast.success(`Product "${foundProduct.name}" successfully published to Nostr!`) + + console.log('🎉 Product confirmed on Nostr with matching content:', { + productId, + productName: foundProduct.name, + pendingTime: Date.now() - timestamp, + contentVerified: true + }) + } else { + console.warn('⚠️ Product appeared in market but content differs:', { + productId, + productName: foundProduct.name, + localData, + marketData + }) + // Remove from pending - content doesn't match, so it's not properly synced + pendingNostrConfirmation.value.delete(productId) + // Don't add to confirmedOnNostr - it should show as unsynced + } + } else { + // No local product found - just mark as confirmed + pendingNostrConfirmation.value.delete(productId) + confirmedOnNostr.value.add(productId) + toast.success(`Product "${foundProduct.name}" successfully published to Nostr!`) + } + } + } + + // Update sync status for any new products that appear in market feed + initializeSyncStatus() +}, { deep: true }) + +// Cleanup pending confirmations after timeout (30 seconds) +const cleanupPendingConfirmations = () => { + const timeout = 30 * 1000 // 30 seconds + const now = Date.now() + + for (const [productId, timestamp] of pendingNostrConfirmation.value.entries()) { + if (now - timestamp > timeout) { + pendingNostrConfirmation.value.delete(productId) + console.warn('⏰ Timeout: Product confirmation removed from pending after 30s:', productId) + } + } +} + +// Run cleanup every 10 seconds +setInterval(cleanupPendingConfirmations, 10 * 1000) + +// Helper function to normalize product data for comparison +const normalizeProductForComparison = (product: any) => { + return { + name: product.name, + description: product.description || '', + price: product.price, + quantity: product.quantity, + active: product.active ?? true, + categories: (product.categories ? [...product.categories] : []).sort(), // Sort for consistent comparison + images: (product.images ? [...product.images] : []).sort(), // Sort for consistent comparison + currency: product.currency || 'sat' + } +} + +// Enhanced sync status detection with JSON content comparison +const initializeSyncStatus = () => { + // Cross-reference stallProducts with market feed to detect already-synced products + for (const product of stallProducts.value) { + if (product.id) { + const foundInMarket = marketStore.products.find(p => p.id === product.id) + if (foundInMarket) { + // Compare the actual product content, not just IDs + const localData = normalizeProductForComparison(product) + const marketData = normalizeProductForComparison(foundInMarket) + + // Deep comparison of normalized data + const localJson = JSON.stringify(localData) + const marketJson = JSON.stringify(marketData) + const isContentSynced = localJson === marketJson + + if (isContentSynced) { + // Product content is truly synced - mark as confirmed + confirmedOnNostr.value.add(product.id) + console.log('✅ Product content verified as synced to Nostr:', { + productId: product.id, + productName: product.name + }) + } else { + // Product exists but content differs - needs re-sync + console.warn('⚠️ Product exists but content differs - needs re-sync:', { + productId: product.id, + productName: product.name, + localData, + marketData, + differences: { + local: localData, + market: marketData + } + }) + // Remove from both confirmed and pending - it's out of sync + confirmedOnNostr.value.delete(product.id) + pendingNostrConfirmation.value.delete(product.id) + // User should see unsynced indicator (no badge) + } + } else { + console.log('📤 Product not found in market feed - not synced:', { + productId: product.id, + productName: product.name + }) + } + } + } +} + // Lifecycle onMounted(async () => { console.log('Merchant Store component loaded') diff --git a/src/modules/market/composables/useMarket.ts b/src/modules/market/composables/useMarket.ts index 63fba57..cadb14b 100644 --- a/src/modules/market/composables/useMarket.ts +++ b/src/modules/market/composables/useMarket.ts @@ -297,6 +297,14 @@ export function useMarket() { .map((tag: any) => tag[1]) .filter((cat: string) => cat && cat.trim()) + // Debug: Log category processing (when categories are present) + if (categories.length > 0) { + console.log('🛒 useMarket: Processing product with categories:', { + productName: productData.name, + processedCategories: categories, + eventTags: latestEvent.tags.filter((tag: string[]) => tag[0] === 't') + }) + } // Look up the stall name from the stalls array const stall = marketStore.stalls.find(s => s.id === stallId) @@ -489,6 +497,15 @@ export function useMarket() { .map((tag: any) => tag[1]) .filter((cat: string) => cat && cat.trim()) + // Debug: Log real-time category processing (when categories are present) + if (categories.length > 0) { + console.log('🛒 useMarket: Real-time product with categories:', { + productName: productData.name, + processedCategories: categories, + eventTags: event.tags.filter((tag: string[]) => tag[0] === 't') + }) + } + // Look up the stall name from the stalls array const stall = marketStore.stalls.find(s => s.id === stallId) const stallName = stall?.name || 'Unknown Stall' @@ -516,17 +533,7 @@ export function useMarket() { } } - // Publish a product - const publishProduct = async (_productData: any) => { - // Implementation would depend on your event creation logic - // TODO: Implement product publishing - } - - // Publish a stall - const publishStall = async (_stallData: any) => { - // Implementation would depend on your event creation logic - // TODO: Implement stall publishing - } + // Publishing methods removed - now handled by LNbits API endpoints // Connect to market const connectToMarket = async () => { @@ -617,8 +624,6 @@ export function useMarket() { connectToMarket, disconnectFromMarket, processPendingProducts, - publishProduct, - publishStall, subscribeToMarketUpdates, subscribeToOrderUpdates } diff --git a/src/modules/market/services/nostrmarketAPI.ts b/src/modules/market/services/nostrmarketAPI.ts index f4cae9f..4913aa0 100644 --- a/src/modules/market/services/nostrmarketAPI.ts +++ b/src/modules/market/services/nostrmarketAPI.ts @@ -99,6 +99,62 @@ export interface CreateStallRequest { } } +// Order related types +export interface OrderItem { + product_id: string + quantity: number +} + +export interface OrderContact { + nostr?: string + phone?: string + email?: string +} + +export interface ProductOverview { + id: string + name: string + price: number + product_shipping_cost?: number +} + +export interface OrderExtra { + products: ProductOverview[] + currency: string + btc_price: string + shipping_cost: number + shipping_cost_sat: number + fail_message?: string +} + +export interface OrderApiResponse { + id: string + event_id?: string + event_created_at?: number + public_key: string + stall_id: string + invoice_id: string + total: number + paid: boolean + shipped: boolean + time?: number + contact_data: string // JSON string + order_items: string // JSON string + extra_data: string // JSON string + address?: string + message?: string + contact?: OrderContact // Parsed from contact_data + items?: OrderItem[] // Parsed from order_items + extra?: OrderExtra // Parsed from extra_data +} + +export interface OrderStatusUpdate { + id: string + message?: string + paid?: boolean + shipped?: boolean +} + export class NostrmarketAPI extends BaseService { // Service metadata protected readonly metadata = { @@ -368,6 +424,20 @@ export class NostrmarketAPI extends BaseService { walletAdminkey: string, productData: CreateProductRequest ): Promise { + // Debug: Log the exact payload being sent + this.debug('Creating product with payload:', { + name: productData.name, + stall_id: productData.stall_id, + categories: productData.categories, + categoriesType: typeof productData.categories, + categoriesLength: productData.categories?.length, + price: productData.price, + quantity: productData.quantity, + active: productData.active, + config: productData.config, + fullPayload: JSON.stringify(productData, null, 2) + }) + const product = await this.request( '/api/v1/product', walletAdminkey, @@ -377,10 +447,12 @@ export class NostrmarketAPI extends BaseService { } ) - this.debug('Created product:', { + this.debug('Created product response:', { productId: product.id, productName: product.name, - stallId: product.stall_id + stallId: product.stall_id, + returnedCategories: product.categories, + returnedCategoriesLength: product.categories?.length }) return product @@ -446,4 +518,153 @@ export class NostrmarketAPI extends BaseService { this.debug('Deleted product:', { productId }) } + + /** + * Get all orders for the merchant + */ + async getOrders( + walletInkey: string, + filters?: { paid?: boolean, shipped?: boolean, pubkey?: string } + ): Promise { + try { + const params = new URLSearchParams() + if (filters?.paid !== undefined) params.append('paid', filters.paid.toString()) + if (filters?.shipped !== undefined) params.append('shipped', filters.shipped.toString()) + if (filters?.pubkey) params.append('pubkey', filters.pubkey) + + const queryString = params.toString() + const endpoint = queryString ? `/api/v1/order?${queryString}` : '/api/v1/order' + + const orders = await this.request( + endpoint, + walletInkey, + { method: 'GET' } + ) + + // The API already returns parsed objects, no need to parse JSON strings + const parsedOrders = (orders || []).map((order, index) => { + // Debug: Log the first order's structure + if (index === 0) { + this.debug('First order structure:', { + id: order.id, + contact: order.contact, + items: order.items, + extra: order.extra, + hasContactData: !!order.contact, + hasItemsData: !!order.items, + hasExtraData: !!order.extra, + hasProductsInExtra: !!(order.extra?.products) + }) + } + + return order + }) + + this.debug('Retrieved orders:', { count: parsedOrders.length, filters }) + return parsedOrders + } catch (error) { + this.debug('Failed to get orders:', error) + return [] + } + } + + /** + * Get orders for a specific stall + */ + async getStallOrders( + walletInkey: string, + stallId: string, + filters?: { paid?: boolean, shipped?: boolean, pubkey?: string } + ): Promise { + try { + const params = new URLSearchParams() + if (filters?.paid !== undefined) params.append('paid', filters.paid.toString()) + if (filters?.shipped !== undefined) params.append('shipped', filters.shipped.toString()) + if (filters?.pubkey) params.append('pubkey', filters.pubkey) + + const queryString = params.toString() + const endpoint = queryString + ? `/api/v1/stall/order/${stallId}?${queryString}` + : `/api/v1/stall/order/${stallId}` + + const orders = await this.request( + endpoint, + walletInkey, + { method: 'GET' } + ) + + // The API already returns parsed objects, no need to parse JSON strings + const parsedOrders = (orders || []).map((order, index) => { + // Debug: Log the first order's structure for stall orders too + if (index === 0) { + this.debug('First stall order structure:', { + id: order.id, + contact: order.contact, + items: order.items, + extra: order.extra, + hasContactData: !!order.contact, + hasItemsData: !!order.items, + hasExtraData: !!order.extra, + hasProductsInExtra: !!(order.extra?.products) + }) + } + + return order + }) + + this.debug('Retrieved stall orders:', { stallId, count: parsedOrders.length, filters }) + return parsedOrders + } catch (error) { + this.debug('Failed to get stall orders:', error) + return [] + } + } + + /** + * Get a single order by ID + */ + async getOrder(walletInkey: string, orderId: string): Promise { + try { + const order = await this.request( + `/api/v1/order/${orderId}`, + walletInkey, + { method: 'GET' } + ) + + // The API already returns parsed objects, no parsing needed + + this.debug('Retrieved order:', { orderId }) + return order + } catch (error) { + this.debug('Failed to get order:', error) + return null + } + } + + /** + * Update order status (mark as paid/shipped) + */ + async updateOrderStatus( + walletAdminkey: string, + statusUpdate: OrderStatusUpdate + ): Promise { + try { + const order = await this.request( + `/api/v1/order/${statusUpdate.id}`, + walletAdminkey, + { + method: 'PATCH', + body: JSON.stringify(statusUpdate) + } + ) + + // The API already returns parsed objects, no parsing needed + + this.debug('Updated order status:', statusUpdate) + return order + } catch (error) { + this.debug('Failed to update order status:', error) + return null + } + } } diff --git a/src/modules/market/services/nostrmarketService.ts b/src/modules/market/services/nostrmarketService.ts index 7606e6b..733879e 100644 --- a/src/modules/market/services/nostrmarketService.ts +++ b/src/modules/market/services/nostrmarketService.ts @@ -1,6 +1,6 @@ import { finalizeEvent, type EventTemplate, nip04 } from 'nostr-tools' import { BaseService } from '@/core/base/BaseService' -import type { Stall, Product, Order } from '@/modules/market/stores/market' +import type { Order } from '@/modules/market/stores/market' export interface NostrmarketStall { id: string @@ -27,6 +27,9 @@ export interface NostrmarketProduct { currency: string } +// Note: Stall and Product publishing is handled by LNbits API endpoints +// NostrmarketService now only handles order DMs and status updates + export interface NostrmarketOrder { id: string items: Array<{ @@ -152,90 +155,8 @@ export class NostrmarketService extends BaseService { } } - /** - * Publish a stall event (kind 30017) to Nostr - */ - async publishStall(stall: Stall): Promise { - const { prvkey } = this.getAuth() - - const stallData: NostrmarketStall = { - id: stall.id, - name: stall.name, - description: stall.description, - currency: stall.currency, - shipping: (stall.shipping || []).map(zone => ({ - id: zone.id, - name: zone.name, - cost: zone.cost, - countries: [] - })) - } - - const eventTemplate: EventTemplate = { - kind: 30017, - tags: [ - ['t', 'stall'], - ['t', 'nostrmarket'] - ], - content: JSON.stringify(stallData), - created_at: Math.floor(Date.now() / 1000) - } - - const prvkeyBytes = this.hexToUint8Array(prvkey) - const event = finalizeEvent(eventTemplate, prvkeyBytes) - const result = await this.relayHub.publishEvent(event) - - console.log('Stall published to nostrmarket:', { - stallId: stall.id, - eventId: result, - content: stallData - }) - - return result.success.toString() - } - - /** - * Publish a product event (kind 30018) to Nostr - */ - async publishProduct(product: Product): Promise { - const { prvkey } = this.getAuth() - - const productData: NostrmarketProduct = { - id: product.id, - stall_id: product.stall_id, - name: product.name, - description: product.description, - images: product.images || [], - categories: product.categories || [], - price: product.price, - quantity: product.quantity, - currency: product.currency - } - - const eventTemplate: EventTemplate = { - kind: 30018, - tags: [ - ['t', 'product'], - ['t', 'nostrmarket'], - ['t', 'stall', product.stall_id], - ...(product.categories || []).map(cat => ['t', cat]) - ], - content: JSON.stringify(productData), - created_at: Math.floor(Date.now() / 1000) - } - - const prvkeyBytes = this.hexToUint8Array(prvkey) - const event = finalizeEvent(eventTemplate, prvkeyBytes) - const result = await this.relayHub.publishEvent(event) - - console.log('Product published to nostrmarket:', { - productId: product.id, - eventId: result, - content: productData - }) - - return result.success.toString() - } + // Removed publishStall() and publishProduct() methods + // Stall and product publishing is now handled by LNbits API endpoints /** * Publish an order event (kind 4 encrypted DM) to nostrmarket @@ -471,38 +392,6 @@ export class NostrmarketService extends BaseService { } } - /** - * Publish all stalls and products for a merchant - */ - async publishMerchantCatalog(stalls: Stall[], products: Product[]): Promise<{ - stalls: Record, // stallId -> eventId - products: Record // productId -> eventId - }> { - const results = { - stalls: {} as Record, - products: {} as Record - } - - // Publish stalls first - for (const stall of stalls) { - try { - const eventId = await this.publishStall(stall) - results.stalls[stall.id] = eventId - } catch (error) { - console.error(`Failed to publish stall ${stall.id}:`, error) - } - } - - // Publish products - for (const product of products) { - try { - const eventId = await this.publishProduct(product) - results.products[product.id] = eventId - } catch (error) { - console.error(`Failed to publish product ${product.id}:`, error) - } - } - - return results - } + // Removed publishMerchantCatalog() method + // Publishing is now handled by LNbits API endpoints } diff --git a/src/modules/market/stores/market.ts b/src/modules/market/stores/market.ts index 20c89f5..b1a11ba 100644 --- a/src/modules/market/stores/market.ts +++ b/src/modules/market/stores/market.ts @@ -470,51 +470,8 @@ export const useMarketStore = defineStore('market', () => { } } - // nostrmarket integration methods - const publishToNostrmarket = async () => { - try { - console.log('Publishing merchant catalog to nostrmarket...') - - // Get all stalls and products - const allStalls = Object.values(stalls.value) - const allProducts = Object.values(products.value) - - if (allStalls.length === 0) { - console.warn('No stalls to publish to nostrmarket') - return null - } - - if (allProducts.length === 0) { - console.warn('No products to publish to nostrmarket') - return null - } - - // Publish to nostrmarket - const result = await nostrmarketService.publishMerchantCatalog(allStalls, allProducts) - - console.log('Successfully published to nostrmarket:', result) - - // Update stalls and products with event IDs - for (const [stallId, eventId] of Object.entries(result.stalls)) { - const stall = stalls.value.find(s => s.id === stallId) - if (stall) { - stall.nostrEventId = eventId - } - } - - for (const [productId, eventId] of Object.entries(result.products)) { - const product = products.value.find(p => p.id === productId) - if (product) { - product.nostrEventId = eventId - } - } - - return result - } catch (error) { - console.error('Failed to publish to nostrmarket:', error) - throw error - } - } + // Removed publishToNostrmarket() method + // Publishing is now handled automatically by LNbits API endpoints // Invoice management methods const createLightningInvoice = async (orderId: string, adminKey: string): Promise => { @@ -916,6 +873,5 @@ export const useMarketStore = defineStore('market', () => { saveOrdersToStorage, loadOrdersFromStorage, clearOrdersForUserChange, - publishToNostrmarket } }) \ No newline at end of file