import { defineStore } from 'pinia' import { ref, computed, readonly, watch } from 'vue' import { nostrOrders } from '@/composables/useNostrOrders' import { invoiceService } from '@/lib/services/invoiceService' import { paymentMonitor } from '@/lib/services/paymentMonitor' import { nostrmarketService } from '../services/nostrmarketService' import { useAuth } from '@/composables/useAuth' import type { LightningInvoice } from '@/lib/services/invoiceService' import type { Market, Stall, Product, Order, ShippingZone, OrderStatus, StallCart, FilterData, SortOptions, PaymentRequest, PaymentStatus } from '../types/market' // Import types that are used in the store implementation export const useMarketStore = defineStore('market', () => { const auth = useAuth() // Helper function to get user-specific storage key const getUserStorageKey = (baseKey: string) => { const userPubkey = auth.currentUser?.value?.pubkey return userPubkey ? `${baseKey}_${userPubkey}` : baseKey } // Core market state const markets = ref([]) const stalls = ref([]) const products = ref([]) const orders = ref>({}) const profiles = ref>({}) // Active selections const activeMarket = ref(null) const activeStall = ref(null) const activeProduct = ref(null) // UI state const isLoading = ref(false) const error = ref(null) const searchText = ref('') const showFilterDetails = ref(false) // Filtering and sorting const filterData = ref({ categories: [], merchants: [], stalls: [], currency: null, priceFrom: null, priceTo: null, inStock: null, paymentMethods: [] }) const sortOptions = ref({ field: 'name', order: 'asc' }) // Enhanced shopping cart with stall-specific carts const stallCarts = ref>({}) // Legacy shopping cart (to be deprecated) const shoppingCart = ref>({}) // Checkout state const checkoutCart = ref(null) const checkoutStall = ref(null) const activeOrder = ref(null) // Payment state const paymentRequest = ref(null) const paymentStatus = ref(null) // Computed properties const filteredProducts = computed(() => { let filtered = products.value // Search filter if (searchText.value) { const searchLower = searchText.value.toLowerCase() filtered = filtered.filter(product => product.name.toLowerCase().includes(searchLower) || product.description?.toLowerCase().includes(searchLower) || product.stallName.toLowerCase().includes(searchLower) ) } // Category filter if (filterData.value.categories.length > 0) { filtered = filtered.filter(product => product.categories?.some(cat => filterData.value.categories.includes(cat)) ) } // Merchant filter if (filterData.value.merchants.length > 0) { filtered = filtered.filter(product => filterData.value.merchants.includes(product.stall_id) ) } // Stall filter if (filterData.value.stalls.length > 0) { filtered = filtered.filter(product => filterData.value.stalls.includes(product.stall_id) ) } // Currency filter if (filterData.value.currency) { filtered = filtered.filter(product => product.currency === filterData.value.currency ) } // Price range filter if (filterData.value.priceFrom !== null) { filtered = filtered.filter(product => product.price >= filterData.value.priceFrom! ) } if (filterData.value.priceTo !== null) { filtered = filtered.filter(product => product.price <= filterData.value.priceTo! ) } // In stock filter if (filterData.value.inStock !== null) { filtered = filtered.filter(product => filterData.value.inStock ? product.quantity > 0 : product.quantity === 0 ) } // Payment methods filter if (filterData.value.paymentMethods.length > 0) { // For now, assume all products support Lightning payments // This can be enhanced later with product-specific payment method support filtered = filtered.filter(_product => true) } // Sort filtered.sort((a, b) => { const aVal = a[sortOptions.value.field as keyof Product] const bVal = b[sortOptions.value.field as keyof Product] if (typeof aVal === 'string' && typeof bVal === 'string') { return sortOptions.value.order === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal) } if (typeof aVal === 'number' && typeof bVal === 'number') { return sortOptions.value.order === 'asc' ? aVal - bVal : bVal - aVal } return 0 }) return filtered }) const allCategories = computed(() => { const categories = new Set() products.value.forEach(product => { product.categories?.forEach(cat => categories.add(cat)) }) return Array.from(categories).map(category => ({ category, count: products.value.filter(p => p.categories?.includes(category)).length, selected: filterData.value.categories.includes(category) })) }) // Enhanced cart computed properties const allStallCarts = computed(() => Object.values(stallCarts.value)) const totalCartItems = computed(() => { return allStallCarts.value.reduce((total, cart) => { return total + cart.products.reduce((cartTotal, item) => cartTotal + item.quantity, 0) }, 0) }) const totalCartValue = computed(() => { return allStallCarts.value.reduce((total, cart) => { return total + cart.subtotal }, 0) }) const activeStallCart = computed(() => { if (!checkoutStall.value) return null return stallCarts.value[checkoutStall.value.id] || null }) // Legacy cart computed properties (to be deprecated) const cartTotal = computed(() => { return Object.values(shoppingCart.value).reduce((total, item) => { return total + (item.product.price * item.quantity) }, 0) }) const cartItemCount = computed(() => { return Object.values(shoppingCart.value).reduce((count, item) => { return count + item.quantity }, 0) }) // Actions const setLoading = (loading: boolean) => { isLoading.value = loading } const setError = (errorMessage: string | null) => { error.value = errorMessage } const setSearchText = (text: string) => { searchText.value = text } const setActiveMarket = (market: Market | null) => { activeMarket.value = market } const setActiveStall = (stall: Stall | null) => { activeStall.value = stall } const setActiveProduct = (product: Product | null) => { activeProduct.value = product } const addProduct = (product: Product) => { const existingIndex = products.value.findIndex(p => p.id === product.id) if (existingIndex >= 0) { products.value[existingIndex] = product } else { products.value.push(product) } } const addStall = (stall: Stall) => { const existingIndex = stalls.value.findIndex(s => s.id === stall.id) if (existingIndex >= 0) { stalls.value[existingIndex] = stall } else { stalls.value.push(stall) } } const addMarket = (market: Market) => { const existingIndex = markets.value.findIndex(m => m.d === market.d) if (existingIndex >= 0) { markets.value[existingIndex] = market } else { markets.value.push(market) } } const addToCart = (product: Product, quantity: number = 1) => { const existing = shoppingCart.value[product.id] if (existing) { existing.quantity += quantity } else { shoppingCart.value[product.id] = { product, quantity } } } const removeFromCart = (productId: string) => { delete shoppingCart.value[productId] } const updateCartQuantity = (productId: string, quantity: number) => { if (quantity <= 0) { removeFromCart(productId) } else { const item = shoppingCart.value[productId] if (item) { item.quantity = quantity } } } const clearCart = () => { shoppingCart.value = {} } // Enhanced cart management methods const addToStallCart = (product: Product, quantity: number = 1) => { const stallId = product.stall_id const stall = stalls.value.find(s => s.id === stallId) if (!stall) { console.error('Stall not found for product:', product.id) return } // Initialize stall cart if it doesn't exist if (!stallCarts.value[stallId]) { stallCarts.value[stallId] = { id: stallId, merchant: stall.pubkey, products: [], subtotal: 0, currency: stall.currency || 'sats' } } const cart = stallCarts.value[stallId] const existingItem = cart.products.find(item => item.product.id === product.id) if (existingItem) { existingItem.quantity = Math.min(existingItem.quantity + quantity, product.quantity) } else { cart.products.push({ product, quantity: Math.min(quantity, product.quantity), stallId }) } // Update cart subtotal updateStallCartSubtotal(stallId) } const removeFromStallCart = (stallId: string, productId: string) => { const cart = stallCarts.value[stallId] if (cart) { cart.products = cart.products.filter(item => item.product.id !== productId) updateStallCartSubtotal(stallId) // Remove empty carts if (cart.products.length === 0) { delete stallCarts.value[stallId] } } } const updateStallCartQuantity = (stallId: string, productId: string, quantity: number) => { const cart = stallCarts.value[stallId] if (cart) { if (quantity <= 0) { removeFromStallCart(stallId, productId) } else { const item = cart.products.find(item => item.product.id === productId) if (item) { item.quantity = Math.min(quantity, item.product.quantity) updateStallCartSubtotal(stallId) } } } } const updateStallCartSubtotal = (stallId: string) => { const cart = stallCarts.value[stallId] if (cart) { cart.subtotal = cart.products.reduce((total, item) => { return total + (item.product.price * item.quantity) }, 0) } } const clearStallCart = (stallId: string) => { delete stallCarts.value[stallId] } const clearAllStallCarts = () => { stallCarts.value = {} } const setCheckoutCart = (stallId: string) => { const cart = stallCarts.value[stallId] const stall = stalls.value.find(s => s.id === stallId) if (cart && stall) { checkoutCart.value = cart checkoutStall.value = stall } } const clearCheckout = () => { checkoutCart.value = null checkoutStall.value = null activeOrder.value = null paymentRequest.value = null paymentStatus.value = null } const setShippingZone = (stallId: string, shippingZone: ShippingZone) => { const cart = stallCarts.value[stallId] if (cart) { cart.shippingZone = shippingZone } } // Order management methods const createOrder = (orderData: Omit & { id?: string }) => { const order: Order = { ...orderData, id: orderData.id || generateOrderId(), createdAt: Math.floor(Date.now() / 1000), updatedAt: Math.floor(Date.now() / 1000) } orders.value[order.id] = order activeOrder.value = order // Save to localStorage for persistence saveOrdersToStorage() return order } const createAndPlaceOrder = async (orderData: Omit) => { try { // Create the order const order = createOrder(orderData) // Attempt to publish order via nostrmarket protocol let nostrmarketSuccess = false let nostrmarketError: string | undefined try { // Publish the order event to nostrmarket using proper protocol const eventId = await nostrmarketService.publishOrder(order, order.sellerPubkey) nostrmarketSuccess = true order.sentViaNostr = true order.nostrEventId = eventId console.log('Order published via nostrmarket successfully:', eventId) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown nostrmarket error' order.nostrError = errorMessage order.sentViaNostr = false console.error('Failed to publish order via nostrmarket:', errorMessage) } // Update order status to 'pending' updateOrderStatus(order.id, 'pending') // Clear the checkout cart if (checkoutCart.value) { clearStallCart(checkoutCart.value.id) } // Clear checkout state clearCheckout() // Show appropriate success/error message if (nostrmarketSuccess) { console.log('Order created and published via nostrmarket successfully') } else { console.warn('Order created but nostrmarket publishing failed:', nostrmarketError) } return order } catch (error) { console.error('Failed to create and place order:', error) throw new Error('Failed to place order. Please try again.') } } // 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 } } // Invoice management methods const createLightningInvoice = async (orderId: string, adminKey: string): Promise => { try { const order = orders.value[orderId] if (!order) { throw new Error('Order not found') } // Create Lightning invoice with admin key and nostrmarket tag // For nostrmarket compatibility, we need to use the original order ID if it exists // If no originalOrderId exists, this order was created in the web-app, so use the current orderId const orderIdForInvoice = order.originalOrderId || orderId console.log('Creating invoice with order ID:', { webAppOrderId: orderId, originalOrderId: order.originalOrderId, orderIdForInvoice: orderIdForInvoice, hasOriginalOrderId: !!order.originalOrderId }) const invoice = await invoiceService.createInvoice(order, adminKey, { tag: "nostrmarket", order_id: orderIdForInvoice, // Use original Nostr order ID for nostrmarket compatibility merchant_pubkey: order.sellerPubkey, buyer_pubkey: order.buyerPubkey }) // Update order with invoice details order.lightningInvoice = invoice order.paymentHash = invoice.payment_hash order.paymentStatus = 'pending' order.paymentRequest = invoice.bolt11 // Use bolt11 field from LNBits response // Save to localStorage after invoice creation saveOrdersToStorage() // Start monitoring payment await paymentMonitor.startMonitoring(order, invoice) // Set up payment update callback paymentMonitor.onPaymentUpdate(orderId, (update) => { handlePaymentUpdate(orderId, update) }) console.log('Lightning invoice created for order:', { orderId, originalOrderId: order.originalOrderId, nostrmarketOrderId: order.originalOrderId || orderId, paymentHash: invoice.payment_hash, amount: invoice.amount }) return invoice } catch (error) { console.error('Failed to create Lightning invoice:', error) throw new Error('Failed to create payment invoice') } } const handlePaymentUpdate = (orderId: string, update: any) => { const order = orders.value[orderId] if (!order) return // Update order payment status order.paymentStatus = update.status if (update.status === 'paid') { order.paidAt = update.paidAt updateOrderStatus(orderId, 'paid') // Send payment confirmation via Nostr sendPaymentConfirmation(order) } // Save to localStorage after payment update saveOrdersToStorage() console.log('Payment status updated for order:', { orderId, status: update.status, amount: update.amount }) } const sendPaymentConfirmation = async (order: Order) => { try { if (!nostrOrders.isReady.value) { console.warn('Nostr not ready for payment confirmation') return } // Create payment confirmation message // const confirmation = { // type: 'payment_confirmation', // orderId: order.id, // paymentHash: order.paymentHash, // amount: order.total, // currency: order.currency, // paidAt: order.paidAt, // message: 'Payment received! Your order is being processed.' // } // Send confirmation to customer await nostrOrders.publishOrderEvent(order, order.buyerPubkey) console.log('Payment confirmation sent via Nostr') } catch (error) { console.error('Failed to send payment confirmation:', error) } } const getOrderInvoice = (orderId: string): LightningInvoice | null => { const order = orders.value[orderId] return order?.lightningInvoice || null } const getOrderPaymentStatus = (orderId: string): 'pending' | 'paid' | 'expired' | null => { const order = orders.value[orderId] return order?.paymentStatus || null } const updateOrderStatus = (orderId: string, status: OrderStatus) => { const order = orders.value[orderId] if (order) { order.status = status order.updatedAt = Date.now() / 1000 saveOrdersToStorage() } } const updateOrder = (orderId: string, updatedOrder: Partial) => { const order = orders.value[orderId] if (order) { Object.assign(order, updatedOrder) order.updatedAt = Date.now() / 1000 saveOrdersToStorage() } } const setPaymentRequest = (request: PaymentRequest) => { paymentRequest.value = request } const setPaymentStatus = (status: PaymentStatus) => { paymentStatus.value = status } // Utility methods const generateOrderId = () => { return `order_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` } // Persistence methods const saveOrdersToStorage = () => { try { const storageKey = getUserStorageKey('market_orders') localStorage.setItem(storageKey, JSON.stringify(orders.value)) console.log('Saved orders to localStorage with key:', storageKey) } catch (error) { console.warn('Failed to save orders to localStorage:', error) } } const loadOrdersFromStorage = () => { try { const storageKey = getUserStorageKey('market_orders') const stored = localStorage.getItem(storageKey) if (stored) { const parsedOrders = JSON.parse(stored) orders.value = parsedOrders console.log('Loaded orders from localStorage with key:', storageKey, 'Orders count:', Object.keys(parsedOrders).length) } else { console.log('No orders found in localStorage for key:', storageKey) // Clear any existing orders when switching users orders.value = {} } } catch (error) { console.warn('Failed to load orders from localStorage:', error) // Clear orders on error orders.value = {} } } // Clear orders when user changes const clearOrdersForUserChange = () => { orders.value = {} console.log('Cleared orders for user change') } // Payment utility methods const calculateOrderTotal = (cart: StallCart, shippingZone?: ShippingZone) => { const subtotal = cart.subtotal const shippingCost = shippingZone?.cost || 0 return subtotal + shippingCost } const validateCartForCheckout = (stallId: string): { valid: boolean; errors: string[] } => { const cart = stallCarts.value[stallId] const errors: string[] = [] if (!cart || cart.products.length === 0) { errors.push('Cart is empty') return { valid: false, errors } } // Check if all products are still in stock for (const item of cart.products) { if (item.quantity > item.product.quantity) { errors.push(`${item.product.name} is out of stock`) } } // Check if cart has shipping zone selected if (!cart.shippingZone) { errors.push('Please select a shipping zone') } return { valid: errors.length === 0, errors } } const getCartSummary = (stallId: string) => { const cart = stallCarts.value[stallId] if (!cart) return null const itemCount = cart.products.reduce((total, item) => total + item.quantity, 0) const subtotal = cart.subtotal const shippingCost = cart.shippingZone?.cost || 0 const total = subtotal + shippingCost return { itemCount, subtotal, shippingCost, total, currency: cart.currency } } const updateFilterData = (newFilterData: Partial) => { filterData.value = { ...filterData.value, ...newFilterData } } const clearFilters = () => { filterData.value = { categories: [], merchants: [], stalls: [], currency: null, priceFrom: null, priceTo: null, inStock: null, paymentMethods: [] } } const toggleCategoryFilter = (category: string) => { const index = filterData.value.categories.indexOf(category) if (index >= 0) { filterData.value.categories.splice(index, 1) } else { filterData.value.categories.push(category) } } const updateSortOptions = (field: string, order: 'asc' | 'desc' = 'asc') => { sortOptions.value = { field, order } } const formatPrice = (price: number, currency: string) => { if (currency === 'sat') { return `${price} sats` } return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency.toUpperCase() }).format(price) } // Initialize orders from localStorage loadOrdersFromStorage() // Watch for user changes and reload orders watch(() => auth.currentUser?.value?.pubkey, (newPubkey, oldPubkey) => { if (newPubkey !== oldPubkey) { console.log('User changed, clearing and reloading orders. Old:', oldPubkey, 'New:', newPubkey) clearOrdersForUserChange() loadOrdersFromStorage() } }) return { // State markets: readonly(markets), stalls: readonly(stalls), products: readonly(products), orders: readonly(orders), profiles: readonly(profiles), activeMarket: readonly(activeMarket), activeStall: readonly(activeStall), activeProduct: readonly(activeProduct), isLoading: readonly(isLoading), error: readonly(error), searchText: readonly(searchText), showFilterDetails: readonly(showFilterDetails), filterData: readonly(filterData), sortOptions: readonly(sortOptions), shoppingCart: readonly(shoppingCart), stallCarts: readonly(stallCarts), checkoutCart: readonly(checkoutCart), checkoutStall: readonly(checkoutStall), activeOrder: readonly(activeOrder), paymentRequest: readonly(paymentRequest), paymentStatus: readonly(paymentStatus), // Computed filteredProducts, allCategories, allStallCarts, totalCartItems, totalCartValue, activeStallCart, cartTotal, cartItemCount, // Actions setLoading, setError, setSearchText, setActiveMarket, setActiveStall, setActiveProduct, addProduct, addStall, addMarket, addToCart, removeFromCart, updateCartQuantity, clearCart, updateFilterData, clearFilters, toggleCategoryFilter, updateSortOptions, formatPrice, addToStallCart, removeFromStallCart, updateStallCartQuantity, updateStallCartSubtotal, clearStallCart, clearAllStallCarts, setCheckoutCart, clearCheckout, setShippingZone, createOrder, updateOrderStatus, setPaymentRequest, setPaymentStatus, generateOrderId, calculateOrderTotal, validateCartForCheckout, getCartSummary, createAndPlaceOrder, createLightningInvoice, handlePaymentUpdate, sendPaymentConfirmation, getOrderInvoice, getOrderPaymentStatus, updateOrder, saveOrdersToStorage, loadOrdersFromStorage, clearOrdersForUserChange, publishToNostrmarket } })