Refactor OrderHistory and introduce payment status checker
- Update OrderHistory.vue to utilize a new method for determining effective order status, enhancing clarity in payment and shipping status display. - Add a new composable, usePaymentStatusChecker, to handle payment status checks via LNbits API, improving order payment verification. - Modify nostrmarketService to streamline order updates with consolidated status handling for paid and shipped states. - Enhance market store initialization to include payment status and paid fields for better order management. - Update market types to include new fields for payment and shipping status, ensuring consistency across the application.
This commit is contained in:
parent
4258ea87c4
commit
99bbde4d05
5 changed files with 230 additions and 34 deletions
|
|
@ -105,16 +105,30 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Paid Section -->
|
||||||
|
<div v-if="isOrderPaid(order)" class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg dark:bg-green-950 dark:border-green-800">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<CheckCircle class="w-5 h-5 text-green-600" />
|
||||||
|
<span class="font-medium text-green-800 dark:text-green-200">Payment Confirmed</span>
|
||||||
|
<Badge variant="default" class="text-xs bg-green-600 text-white">
|
||||||
|
Paid
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p v-if="order.paidAt" class="text-sm text-green-700 dark:text-green-300 mt-1">
|
||||||
|
Paid on {{ formatDate(order.paidAt) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Payment Section -->
|
<!-- Payment Section -->
|
||||||
<div v-if="order.paymentRequest" class="mb-4 p-4 bg-muted/50 border border-border rounded-lg">
|
<div v-if="order.paymentRequest && !isOrderPaid(order)" class="mb-4 p-4 bg-muted/50 border border-border rounded-lg">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Zap class="w-4 h-4 text-yellow-500" />
|
<Zap class="w-4 h-4 text-yellow-500" />
|
||||||
<span class="font-medium text-foreground">Payment Required</span>
|
<span class="font-medium text-foreground">Payment Required</span>
|
||||||
</div>
|
</div>
|
||||||
<Badge :variant="order.paymentStatus === 'paid' ? 'default' : 'secondary'" class="text-xs"
|
<Badge :variant="isOrderPaid(order) ? 'default' : 'secondary'" class="text-xs"
|
||||||
:class="order.paymentStatus === 'paid' ? 'text-green-600' : 'text-amber-600'">
|
:class="isOrderPaid(order) ? 'text-green-600' : 'text-amber-600'">
|
||||||
{{ order.paymentStatus === 'paid' ? 'Paid' : 'Pending' }}
|
{{ isOrderPaid(order) ? 'Paid' : 'Pending' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -237,7 +251,7 @@ import { auth } from '@/composables/useAuth'
|
||||||
import { useLightningPayment } from '../composables/useLightningPayment'
|
import { useLightningPayment } from '../composables/useLightningPayment'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Package, Store, Zap, Copy, QrCode } from 'lucide-vue-next'
|
import { Package, Store, Zap, Copy, QrCode, CheckCircle } from 'lucide-vue-next'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import type { OrderStatus } from '@/stores/market'
|
import type { OrderStatus } from '@/stores/market'
|
||||||
|
|
||||||
|
|
@ -286,11 +300,20 @@ const sortedOrders = computed(() => {
|
||||||
const totalOrders = computed(() => allOrders.value.length)
|
const totalOrders = computed(() => allOrders.value.length)
|
||||||
const pendingOrders = computed(() => allOrders.value.filter(o => o.status === 'pending').length)
|
const pendingOrders = computed(() => allOrders.value.filter(o => o.status === 'pending').length)
|
||||||
const paidOrders = computed(() => allOrders.value.filter(o => o.status === 'paid').length)
|
const paidOrders = computed(() => allOrders.value.filter(o => o.status === 'paid').length)
|
||||||
const pendingPayments = computed(() => allOrders.value.filter(o => o.paymentStatus === 'pending').length)
|
const pendingPayments = computed(() => allOrders.value.filter(o => !isOrderPaid(o)).length)
|
||||||
|
|
||||||
const isDevelopment = computed(() => import.meta.env.DEV)
|
const isDevelopment = computed(() => import.meta.env.DEV)
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
|
const isOrderPaid = (order: Order) => {
|
||||||
|
// Prioritize the 'paid' field from Nostr status updates (type 2)
|
||||||
|
if (order.paid !== undefined) {
|
||||||
|
return order.paid
|
||||||
|
}
|
||||||
|
// Fallback to paymentStatus field
|
||||||
|
return order.paymentStatus === 'paid'
|
||||||
|
}
|
||||||
|
|
||||||
const formatDate = (timestamp: number) => {
|
const formatDate = (timestamp: number) => {
|
||||||
return new Date(timestamp * 1000).toLocaleDateString('en-US', {
|
return new Date(timestamp * 1000).toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
|
|
||||||
139
src/modules/market/composables/usePaymentStatusChecker.ts
Normal file
139
src/modules/market/composables/usePaymentStatusChecker.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useAuth } from '@/composables/useAuth'
|
||||||
|
import { useMarketStore } from '../stores/market'
|
||||||
|
|
||||||
|
// Simplified bolt11 parser to extract payment hash
|
||||||
|
function parseBolt11(bolt11: string): { paymentHash?: string } {
|
||||||
|
try {
|
||||||
|
// Remove lightning: prefix if present
|
||||||
|
const cleanBolt11 = bolt11.replace(/^lightning:/, '')
|
||||||
|
|
||||||
|
// Very basic bolt11 parsing - in a real app you'd use a proper library
|
||||||
|
// For now, we'll return empty since this requires complex bech32 decoding
|
||||||
|
return {}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse bolt11:', error)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePaymentStatusChecker() {
|
||||||
|
const { currentUser } = useAuth()
|
||||||
|
const marketStore = useMarketStore()
|
||||||
|
const isChecking = ref(false)
|
||||||
|
|
||||||
|
// Check payment status via LNbits API using payment hash
|
||||||
|
async function checkPaymentStatusByHash(paymentHash: string, walletId?: string): Promise<boolean> {
|
||||||
|
if (!currentUser.value || !paymentHash) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get wallet with admin key
|
||||||
|
const wallet = walletId
|
||||||
|
? currentUser.value.wallets?.find((w: any) => w.id === walletId)
|
||||||
|
: currentUser.value.wallets?.find((w: any) => w.adminkey) // Use first wallet with admin key
|
||||||
|
|
||||||
|
if (!wallet || !wallet.adminkey) {
|
||||||
|
console.warn('No wallet with admin key found for payment status check')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check payment status via LNbits API
|
||||||
|
const response = await fetch(`/api/v1/payments/${paymentHash}`, {
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'X-API-KEY': wallet.adminkey,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const payment = await response.json()
|
||||||
|
return payment.paid === true
|
||||||
|
} else {
|
||||||
|
console.warn('Payment not found or API error:', response.status)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check payment status:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check payment status for a bolt11 invoice
|
||||||
|
async function checkPaymentStatusByBolt11(bolt11: string): Promise<boolean> {
|
||||||
|
const parsed = parseBolt11(bolt11)
|
||||||
|
if (parsed.paymentHash) {
|
||||||
|
return await checkPaymentStatusByHash(parsed.paymentHash)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and update payment status for an order
|
||||||
|
async function checkAndUpdateOrderPaymentStatus(orderId: string): Promise<boolean> {
|
||||||
|
if (isChecking.value) return false
|
||||||
|
|
||||||
|
const order = marketStore.orders[orderId]
|
||||||
|
if (!order || !order.paymentRequest) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isChecking.value = true
|
||||||
|
console.log('🔍 Checking payment status for order:', orderId.slice(-8))
|
||||||
|
|
||||||
|
// Method 1: Check by bolt11 (if we can extract payment hash)
|
||||||
|
const isPaid = await checkPaymentStatusByBolt11(order.paymentRequest)
|
||||||
|
|
||||||
|
if (isPaid && order.paymentStatus !== 'paid') {
|
||||||
|
console.log('✅ Payment confirmed via API check for order:', orderId.slice(-8))
|
||||||
|
|
||||||
|
// Update order status
|
||||||
|
const updatedOrder = {
|
||||||
|
...order,
|
||||||
|
status: 'paid' as const,
|
||||||
|
paymentStatus: 'paid' as const,
|
||||||
|
paidAt: Math.floor(Date.now() / 1000),
|
||||||
|
items: [...order.items] // Convert readonly to mutable
|
||||||
|
}
|
||||||
|
|
||||||
|
marketStore.updateOrder(orderId, updatedOrder)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return isPaid
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check payment status:', error)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
isChecking.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check payment status for all pending orders
|
||||||
|
async function checkAllPendingOrders(): Promise<void> {
|
||||||
|
const pendingOrders = Object.values(marketStore.orders).filter(
|
||||||
|
order => order.paymentStatus === 'pending' && order.paymentRequest
|
||||||
|
)
|
||||||
|
|
||||||
|
if (pendingOrders.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔍 Checking payment status for ${pendingOrders.length} pending orders`)
|
||||||
|
|
||||||
|
for (const order of pendingOrders) {
|
||||||
|
await checkAndUpdateOrderPaymentStatus(order.id)
|
||||||
|
// Add small delay between requests to avoid overwhelming the API
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isChecking,
|
||||||
|
checkPaymentStatusByHash,
|
||||||
|
checkPaymentStatusByBolt11,
|
||||||
|
checkAndUpdateOrderPaymentStatus,
|
||||||
|
checkAllPendingOrders
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -411,39 +411,39 @@ export class NostrmarketService {
|
||||||
)
|
)
|
||||||
|
|
||||||
if (order) {
|
if (order) {
|
||||||
// Update order status
|
// Create the updated order object with all status updates
|
||||||
|
const updatedOrder = {
|
||||||
|
...order,
|
||||||
|
updatedAt: Math.floor(Date.now() / 1000),
|
||||||
|
items: [...order.items] // Convert readonly to mutable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update payment status
|
||||||
if (statusUpdate.paid !== undefined) {
|
if (statusUpdate.paid !== undefined) {
|
||||||
const newStatus = statusUpdate.paid ? 'paid' : 'pending'
|
updatedOrder.paid = statusUpdate.paid
|
||||||
marketStore.updateOrderStatus(order.id, newStatus)
|
updatedOrder.paymentStatus = (statusUpdate.paid ? 'paid' : 'pending') as 'paid' | 'pending' | 'expired'
|
||||||
|
updatedOrder.status = statusUpdate.paid ? 'paid' : 'pending'
|
||||||
// Also update payment status
|
updatedOrder.paidAt = statusUpdate.paid ? Math.floor(Date.now() / 1000) : undefined
|
||||||
const updatedOrder = {
|
|
||||||
...order,
|
|
||||||
paymentStatus: (statusUpdate.paid ? 'paid' : 'pending') as 'paid' | 'pending' | 'expired',
|
|
||||||
paidAt: statusUpdate.paid ? Math.floor(Date.now() / 1000) : undefined,
|
|
||||||
updatedAt: Math.floor(Date.now() / 1000),
|
|
||||||
items: [...order.items] // Convert readonly to mutable
|
|
||||||
}
|
|
||||||
marketStore.updateOrder(order.id, updatedOrder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update shipping status
|
||||||
if (statusUpdate.shipped !== undefined) {
|
if (statusUpdate.shipped !== undefined) {
|
||||||
// Update shipping status if you have that field
|
updatedOrder.shipped = statusUpdate.shipped
|
||||||
const updatedOrder = {
|
if (statusUpdate.shipped && updatedOrder.status === 'paid') {
|
||||||
...order,
|
updatedOrder.status = 'shipped'
|
||||||
shipped: statusUpdate.shipped,
|
|
||||||
status: statusUpdate.shipped ? 'shipped' : order.status,
|
|
||||||
updatedAt: Math.floor(Date.now() / 1000),
|
|
||||||
items: [...order.items] // Convert readonly to mutable
|
|
||||||
}
|
}
|
||||||
marketStore.updateOrder(order.id, updatedOrder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Order status updated:', {
|
// Apply the update - this should trigger persistence
|
||||||
|
marketStore.updateOrder(order.id, updatedOrder)
|
||||||
|
|
||||||
|
console.log('Order status updated and persisted:', {
|
||||||
orderId: statusUpdate.id,
|
orderId: statusUpdate.id,
|
||||||
paid: statusUpdate.paid,
|
paid: updatedOrder.paid,
|
||||||
shipped: statusUpdate.shipped,
|
shipped: updatedOrder.shipped,
|
||||||
newStatus: statusUpdate.paid ? 'paid' : 'pending'
|
paymentStatus: updatedOrder.paymentStatus,
|
||||||
|
status: updatedOrder.status,
|
||||||
|
paidAt: updatedOrder.paidAt
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
console.warn('Status update received for unknown order:', statusUpdate.id)
|
console.warn('Status update received for unknown order:', statusUpdate.id)
|
||||||
|
|
|
||||||
|
|
@ -403,7 +403,9 @@ export const useMarketStore = defineStore('market', () => {
|
||||||
...orderData,
|
...orderData,
|
||||||
id: orderData.id || generateOrderId(),
|
id: orderData.id || generateOrderId(),
|
||||||
createdAt: Math.floor(Date.now() / 1000),
|
createdAt: Math.floor(Date.now() / 1000),
|
||||||
updatedAt: Math.floor(Date.now() / 1000)
|
updatedAt: Math.floor(Date.now() / 1000),
|
||||||
|
paid: false, // Initialize as unpaid
|
||||||
|
paymentStatus: orderData.paymentStatus || 'pending' // Default to pending if not specified
|
||||||
}
|
}
|
||||||
|
|
||||||
orders.value[order.id] = order
|
orders.value[order.id] = order
|
||||||
|
|
@ -682,7 +684,22 @@ export const useMarketStore = defineStore('market', () => {
|
||||||
try {
|
try {
|
||||||
const storageKey = getUserStorageKey('market_orders')
|
const storageKey = getUserStorageKey('market_orders')
|
||||||
localStorage.setItem(storageKey, JSON.stringify(orders.value))
|
localStorage.setItem(storageKey, JSON.stringify(orders.value))
|
||||||
console.log('Saved orders to localStorage with key:', storageKey)
|
|
||||||
|
// Debug: Check what's being saved
|
||||||
|
const orderCount = Object.keys(orders.value).length
|
||||||
|
const paidOrders = Object.values(orders.value).filter(o => o.paymentStatus === 'paid' || o.status === 'paid')
|
||||||
|
|
||||||
|
console.log('💾 Saved orders to localStorage:', {
|
||||||
|
storageKey,
|
||||||
|
totalOrders: orderCount,
|
||||||
|
paidOrders: paidOrders.length,
|
||||||
|
orderStatuses: Object.values(orders.value).map(o => ({
|
||||||
|
id: o.id?.slice(-8),
|
||||||
|
status: o.status,
|
||||||
|
paymentStatus: o.paymentStatus,
|
||||||
|
hasPaymentRequest: !!o.paymentRequest
|
||||||
|
}))
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to save orders to localStorage:', error)
|
console.warn('Failed to save orders to localStorage:', error)
|
||||||
}
|
}
|
||||||
|
|
@ -695,7 +712,22 @@ export const useMarketStore = defineStore('market', () => {
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const parsedOrders = JSON.parse(stored)
|
const parsedOrders = JSON.parse(stored)
|
||||||
orders.value = parsedOrders
|
orders.value = parsedOrders
|
||||||
console.log('Loaded orders from localStorage with key:', storageKey, 'Orders count:', Object.keys(parsedOrders).length)
|
|
||||||
|
// Debug: Check payment status of loaded orders
|
||||||
|
const orderCount = Object.keys(parsedOrders).length
|
||||||
|
const paidOrders = Object.values(parsedOrders).filter((o: any) => o.paymentStatus === 'paid' || o.status === 'paid')
|
||||||
|
|
||||||
|
console.log('📦 Loaded orders from localStorage:', {
|
||||||
|
storageKey,
|
||||||
|
totalOrders: orderCount,
|
||||||
|
paidOrders: paidOrders.length,
|
||||||
|
orderStatuses: Object.values(parsedOrders).map((o: any) => ({
|
||||||
|
id: o.id?.slice(-8),
|
||||||
|
status: o.status,
|
||||||
|
paymentStatus: o.paymentStatus,
|
||||||
|
hasPaymentRequest: !!o.paymentRequest
|
||||||
|
}))
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log('No orders found in localStorage for key:', storageKey)
|
console.log('No orders found in localStorage for key:', storageKey)
|
||||||
// Clear any existing orders when switching users
|
// Clear any existing orders when switching users
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ export interface Order {
|
||||||
paymentHash?: string
|
paymentHash?: string
|
||||||
paidAt?: number
|
paidAt?: number
|
||||||
paymentStatus?: 'pending' | 'paid' | 'expired'
|
paymentStatus?: 'pending' | 'paid' | 'expired'
|
||||||
|
paid?: boolean // Direct boolean field matching nostrmarket reference
|
||||||
|
shipped?: boolean // Shipping status from merchant updates
|
||||||
qrCodeDataUrl?: string
|
qrCodeDataUrl?: string
|
||||||
qrCodeLoading?: boolean
|
qrCodeLoading?: boolean
|
||||||
qrCodeError?: string | null
|
qrCodeError?: string | null
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue