import { defineStore } from 'pinia' import { ref, computed, readonly, watch } from 'vue' 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 (!nostrmarketService.isReady) { 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 nostrmarketService.publishOrder(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, clearCheckoutCart: clearCheckout, // Alias for consistency setShippingZone, createOrder, updateOrderStatus, setPaymentRequest, setPaymentStatus, generateOrderId, calculateOrderTotal, validateCartForCheckout, getCartSummary, createAndPlaceOrder, createLightningInvoice, handlePaymentUpdate, sendPaymentConfirmation, getOrderInvoice, getOrderPaymentStatus, updateOrder, saveOrdersToStorage, loadOrdersFromStorage, clearOrdersForUserChange, publishToNostrmarket } })