diff --git a/NOSTR_ARCHITECTURE.md b/NOSTR_ARCHITECTURE.md index 4ff0c7f..550eb22 100644 --- a/NOSTR_ARCHITECTURE.md +++ b/NOSTR_ARCHITECTURE.md @@ -780,3 +780,330 @@ This architecture makes it easy to add new Nostr functionality: 8. **Metrics and Monitoring**: Add comprehensive metrics for relay performance This architecture makes the app more maintainable, performant, and user-friendly while providing a solid foundation for future features. + +## Market Integration Roadmap + +### Overview +This document outlines the roadmap for integrating the nostr-market-app purchasing functionality into the web-app, creating a seamless e-commerce experience while maintaining the decentralized, Nostr-based architecture. + +### Analysis of nostr-market-app Purchasing Flow + +The nostr-market-app has a sophisticated purchasing system with the following key components: + +#### 1. Shopping Cart System +- **Cart Management**: Products are added to stall-specific carts +- **Cart State**: Each stall has its own cart with products and quantities +- **Cart Persistence**: Cart data is stored locally and synced across sessions + +#### 2. Checkout Process +- **Order Confirmation**: Users provide contact information (address, email, message) +- **Shipping Selection**: Multiple shipping zones with different costs +- **Payment Options**: Lightning Network, BTC Onchain, and Cashu support + +#### 3. Order Placement +- **Encrypted Communication**: Orders are encrypted using NIP-04 and sent as direct messages +- **Order Structure**: Includes product details, quantities, shipping, and contact info +- **Payment Integration**: Lightning invoice generation and QR code display + +#### 4. Key Components +- `ShoppingCartCheckout.vue` - Main checkout interface +- `useShoppingCart.js` - Cart management logic +- `useOrders.js` - Order placement and management +- `marketStore.js` - Central state management + +### Implementation Roadmap + +#### Phase 1: Enhanced Shopping Cart System (High Priority) + +**1.1 Extend Market Store** +- Add cart management with stall-specific carts +- Implement cart persistence and synchronization +- Add shipping zone support +- Extend existing `useMarketStore` with cart functionality + +**1.2 Create Cart Components** +- `ShoppingCart.vue` - Cart overview and management +- `CartItem.vue` - Individual cart item with quantity controls +- `CartSummary.vue` - Cart totals and checkout button +- Integrate cart icon in header with item count + +**1.3 Cart State Management** +- Implement stall-specific cart structure +- Add cart persistence to local storage +- Sync cart state across components +- Handle cart updates and real-time synchronization + +#### Phase 2: Checkout System (High Priority) + +**2.1 Checkout Flow** +- `CheckoutPage.vue` - Main checkout interface +- Contact information form (address, email, message) +- Shipping zone selection with cost calculation +- Order summary and confirmation + +**2.2 Payment Integration** +- Lightning Network invoice generation +- QR code display for payments +- Payment status tracking +- Integration with existing payment infrastructure + +**2.3 Checkout State Management** +- Form validation and error handling +- Multi-step checkout process +- Order confirmation and review + +#### Phase 3: Order Management (Medium Priority) + +**3.1 Order Processing** +- Encrypted order creation using NIP-04 +- Direct message sending to merchants +- Order status tracking and updates +- Integration with existing Nostr messaging system + +**3.2 Order History** +- `OrdersPage.vue` - User's order history +- Order status updates and notifications +- Communication with merchants +- Order filtering and search + +**3.3 Order Communication** +- Encrypted messaging between buyers and sellers +- Order status notifications +- Shipping updates and tracking + +#### Phase 4: Enhanced User Experience (Medium Priority) + +**4.1 Streamlined Navigation** +- Integrated cart icon in header +- Quick checkout from product cards +- Seamless flow between browsing and purchasing +- Breadcrumb navigation for checkout process + +**4.2 Real-time Updates** +- Live inventory updates +- Order status notifications +- Chat integration with merchants +- WebSocket connections for live updates + +#### Phase 5: Advanced Features (Low Priority) + +**5.1 Multiple Payment Methods** +- BTC Onchain payments +- Cashu integration +- Payment method selection +- Payment preference storage + +**5.2 Advanced Filtering and Search** +- Enhanced product search +- Advanced filtering options +- Saved search preferences +- Product recommendations + +**5.3 Merchant Tools** +- Merchant dashboard +- Inventory management +- Order fulfillment tools +- Analytics and reporting + +### Technical Implementation Details + +#### State Management Architecture + +**Extended Market Store Structure** +```typescript +interface CartItem { + product: Product + quantity: number + stallId: string +} + +interface StallCart { + id: string + merchant: string + products: CartItem[] + subtotal: number + shippingZone?: ShippingZone +} + +interface Order { + id: string + cartId: string + status: OrderStatus + contactInfo: ContactInfo + shippingZone: ShippingZone + paymentRequest?: string + createdAt: number + updatedAt: number +} +``` + +#### Component Architecture + +**New Components to Create** +1. `ShoppingCart.vue` - Main cart interface +2. `CartItem.vue` - Individual cart item +3. `CartSummary.vue` - Cart totals and checkout +4. `CheckoutPage.vue` - Complete checkout flow +5. `OrderSummary.vue` - Order review and confirmation +6. `PaymentModal.vue` - Payment processing interface +7. `OrdersPage.vue` - Order history and management + +**Enhanced Existing Components** +1. `ProductCard.vue` - Add quick add to cart +2. `Market.vue` - Integrate cart functionality +3. Header navigation - Add cart icon and count + +#### Data Flow + +**Cart Management Flow** +1. User adds product to cart +2. Cart state updated in store +3. Cart persisted to local storage +4. Cart UI components updated +5. Real-time sync across components + +**Checkout Flow** +1. User initiates checkout from cart +2. Contact information collected +3. Shipping zone selected +4. Order summary displayed +5. Payment method selected +6. Order encrypted and sent +7. Payment processed +8. Order confirmed + +#### Integration Points + +**Existing Systems** +1. **Authentication**: Integrate with existing auth system +2. **Nostr Store**: Extend existing Nostr functionality +3. **Relay Hub**: Use existing relay connections +4. **Notifications**: Leverage existing notification system +5. **Storage**: Extend existing storage mechanisms + +**New Systems** +1. **Payment Gateway**: Lightning Network integration +2. **Order Management**: Encrypted order processing +3. **Cart Persistence**: Local storage with sync +4. **Real-time Updates**: WebSocket connections + +### Security Considerations + +#### Data Encryption +- All order data encrypted using NIP-04 +- Private keys never stored in plain text +- Secure communication channels +- Payment information protection + +#### Privacy Protection +- Minimal data collection +- User consent for data sharing +- Anonymity options for users +- Secure storage practices + +#### Payment Security +- Lightning Network security +- Payment verification +- Fraud prevention measures +- Secure key management + +### Performance Considerations + +#### Optimization Strategies +- Lazy loading of cart components +- Efficient state management +- Minimal re-renders +- Optimized storage operations + +#### Scalability +- Modular component architecture +- Efficient data structures +- Caching strategies +- Performance monitoring + +### Testing Strategy + +#### Unit Testing +- Component functionality +- Store actions and mutations +- Utility functions +- Integration points + +#### Integration Testing +- End-to-end checkout flow +- Payment processing +- Order management +- Real-time updates + +#### User Testing +- Usability testing +- Performance testing +- Security testing +- Accessibility testing + +### Deployment and Rollout + +#### Phase 1 Deployment +- Enhanced shopping cart +- Basic checkout functionality +- Order placement system + +#### Phase 2 Deployment +- Payment integration +- Order tracking +- Enhanced user experience + +#### Phase 3 Deployment +- Advanced features +- Performance optimizations +- Full feature set + +### Success Metrics + +#### User Experience Metrics +- Cart abandonment rate +- Checkout completion rate +- Time to complete purchase +- User satisfaction scores + +#### Technical Metrics +- Page load times +- Cart sync performance +- Order processing speed +- Error rates + +#### Business Metrics +- Conversion rates +- Average order value +- Repeat purchase rate +- Customer retention + +### Future Enhancements + +#### Long-term Vision +- Multi-currency support +- Advanced analytics +- AI-powered recommendations +- Mobile app development +- API for third-party integrations + +#### Scalability Plans +- Microservices architecture +- Distributed storage +- Global relay network +- Cross-platform support + +### Conclusion + +This roadmap provides a comprehensive plan for integrating the nostr-market-app purchasing functionality into the web-app. The phased approach ensures core functionality is delivered first while building toward a full-featured e-commerce platform. + +The integration will maintain the decentralized, Nostr-based architecture while providing a professional, user-friendly shopping experience. Each phase builds upon the previous one, ensuring a smooth development process and consistent user experience. + +Key success factors include: +- Maintaining the existing architecture and design patterns +- Ensuring seamless integration with current systems +- Prioritizing user experience and performance +- Implementing robust security measures +- Creating a scalable and maintainable codebase + +This roadmap serves as a living document that should be updated as development progresses and new requirements emerge. diff --git a/ORDER_MANAGEMENT_FULFILLMENT.md b/ORDER_MANAGEMENT_FULFILLMENT.md new file mode 100644 index 0000000..af79620 --- /dev/null +++ b/ORDER_MANAGEMENT_FULFILLMENT.md @@ -0,0 +1,921 @@ +# Order Management & Fulfillment Workflows + +This document provides comprehensive coverage of the complete order lifecycle, from initial placement through payment processing to final fulfillment and shipping management. It includes detailed analysis of both merchant and customer interfaces, database operations, and automated fulfillment processes. + +## Overview: Order Lifecycle Management + +The marketplace implements a **comprehensive order management system** with dual interfaces for merchants and customers, supporting complete order tracking from placement to fulfillment with automated inventory management and payment processing. + +### Order States and Transitions + +```mermaid +graph LR + A[Order Created] --> B[Invoice Generated] + B --> C[Payment Request Sent] + C --> D{Payment Status} + D -->|Paid| E[Payment Confirmed] + D -->|Unpaid| F[Invoice Expired/Reissued] + F --> C + E --> G[Inventory Updated] + G --> H{Fulfillment} + H -->|Ready| I[Shipped Status] + H -->|Issue| J[Error/Refund] + I --> K[Order Complete] +``` + +## Core Order Data Models + +### 1. Order Schema (`models.py:400-485`) + +#### Order Structure Hierarchy +```python +class OrderItem(BaseModel): + product_id: str # Product identifier from order + quantity: int # Quantity ordered + +class OrderContact(BaseModel): + nostr: Optional[str] = None # Customer's nostr pubkey + phone: Optional[str] = None # Customer phone number + email: Optional[str] = None # Customer email address + +class OrderExtra(BaseModel): + products: List[ProductOverview] # Snapshot of products at time of order + currency: str # Pricing currency (USD, EUR, sat, etc.) + btc_price: str # Exchange rate at time of order + shipping_cost: float = 0 # Shipping cost in currency + shipping_cost_sat: float = 0 # Shipping cost in satoshis + fail_message: Optional[str] = None # Error message if order failed +``` + +#### Complete Order Model +```python +class Order(PartialOrder): + stall_id: str # Associated stall identifier + invoice_id: str # Lightning invoice payment hash + total: float # Total amount in satoshis + paid: bool = False # Payment status + shipped: bool = False # Shipping/fulfillment status + time: Optional[int] = None # Completion timestamp + extra: OrderExtra # Additional order metadata +``` + +### 2. Order Status Models (`models.py:467-485`) + +#### Status Update Structure +```python +class OrderStatusUpdate(BaseModel): + id: str # Order identifier + message: Optional[str] = None # Status update message + paid: Optional[bool] = False # Payment status + shipped: Optional[bool] = None # Shipping status + +class OrderReissue(BaseModel): + id: str # Order identifier to reissue + shipping_id: Optional[str] = None # Updated shipping zone + +class PaymentRequest(BaseModel): + id: str # Order identifier + message: Optional[str] = None # Response message + payment_options: List[PaymentOption] # Available payment methods +``` + +### 3. Database Schema (`migrations.py:110-130`) + +#### Order Table Structure +```sql +CREATE TABLE nostrmarket.orders ( + merchant_id TEXT NOT NULL, -- Merchant who owns this order + id TEXT PRIMARY KEY, -- Unique order identifier (UUID) + event_id TEXT, -- Nostr event ID for order placement + event_created_at INTEGER NOT NULL, -- Unix timestamp of order creation + public_key TEXT NOT NULL, -- Customer's public key + merchant_public_key TEXT NOT NULL, -- Merchant's public key + contact_data TEXT NOT NULL DEFAULT '{}', -- JSON contact information + extra_data TEXT NOT NULL DEFAULT '{}', -- JSON extra metadata + order_items TEXT NOT NULL, -- JSON array of ordered items + address TEXT, -- Shipping address (deprecated) + total REAL NOT NULL, -- Total amount in satoshis + shipping_id TEXT NOT NULL, -- Shipping zone identifier + stall_id TEXT NOT NULL, -- Associated stall identifier + invoice_id TEXT NOT NULL, -- Lightning invoice payment hash + paid BOOLEAN NOT NULL DEFAULT false, -- Payment confirmation + shipped BOOLEAN NOT NULL DEFAULT false, -- Fulfillment status + time INTEGER -- Completion timestamp +); +``` + +## Merchant Order Management Interface + +### 1. Order List Component (`order-list.js`) + +#### Component Structure and Properties +```javascript +window.app.component('order-list', { + name: 'order-list', + props: ['stall-id', 'customer-pubkey-filter', 'adminkey', 'inkey'], + template: '#order-list', + delimiters: ['${', '}'], +``` + +#### Advanced Search and Filtering (`order-list.js:15-49`) +```javascript +data: function () { + return { + orders: [], + selectedOrder: null, + search: { + publicKey: null, // Filter by customer public key + isPaid: {label: 'All', id: null}, // Payment status filter + isShipped: {label: 'All', id: null}, // Shipping status filter + }, + ternaryOptions: [ + {label: 'All', id: null}, // Show all orders + {label: 'Yes', id: 'true'}, // Filter for paid/shipped = true + {label: 'No', id: 'false'} // Filter for paid/shipped = false + ] + } +} +``` + +#### Dynamic Order Fetching (`order-list.js:156-181`) +```javascript +getOrders: async function () { + try { + // Support both stall-specific and merchant-wide queries + const ordersPath = this.stallId + ? `stall/order/${this.stallId}` // Orders for specific stall + : 'order' // All orders for merchant + + // Build query parameters for filtering + const query = [] + if (this.search.publicKey) { + query.push(`pubkey=${this.search.publicKey}`) + } + if (this.search.isPaid.id) { + query.push(`paid=${this.search.isPaid.id}`) + } + if (this.search.isShipped.id) { + query.push(`shipped=${this.search.isShipped.id}`) + } + + const {data} = await LNbits.api.request( + 'GET', + `/nostrmarket/api/v1/${ordersPath}?${query.join('&')}`, + this.inkey + ) + this.orders = data.map(s => ({...s, expanded: false})) + } catch (error) { + LNbits.utils.notifyApiError(error) + } +} +``` + +### 2. Order Display and Calculations (`order-list.js:119-155`) + +#### Product Information Retrieval +```javascript +productName: function (order, productId) { + product = order.extra.products.find(p => p.id === productId) + if (product) { + return product.name + } + return '' +}, + +productPrice: function (order, productId) { + product = order.extra.products.find(p => p.id === productId) + if (product) { + return `${product.price} ${order.extra.currency}` + } + return '' +}, + +orderTotal: function (order) { + // Calculate total from individual product costs + shipping + const productCost = order.items.reduce((t, item) => { + product = order.extra.products.find(p => p.id === item.product_id) + return t + item.quantity * product.price + }, 0) + return productCost + order.extra.shipping_cost +} +``` + +### 3. Shipping Status Management (`order-list.js:259-280`) + +#### Shipping Status Updates +```javascript +updateOrderShipped: async function () { + this.selectedOrder.shipped = !this.selectedOrder.shipped + try { + await LNbits.api.request( + 'PATCH', + `/nostrmarket/api/v1/order/${this.selectedOrder.id}`, + this.adminkey, + { + id: this.selectedOrder.id, + message: this.shippingMessage, // Custom message to customer + shipped: this.selectedOrder.shipped // New shipping status + } + ) + this.$q.notify({ + type: 'positive', + message: 'Order updated!' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + this.showShipDialog = false +} +``` + +#### Shipping Dialog Interface (`order-list.js:356-365`) +```javascript +showShipOrderDialog: function (order) { + this.selectedOrder = order + this.shippingMessage = order.shipped + ? 'The order has been shipped!' + : 'The order has NOT yet been shipped!' + + // Toggle status (will be confirmed on dialog submit) + this.selectedOrder.shipped = !order.shipped + this.showShipDialog = true +} +``` + +### 4. Order Recovery and Restoration (`order-list.js:194-233`) + +#### Individual Order Restoration +```javascript +restoreOrder: async function (eventId) { + try { + this.search.restoring = true + const {data} = await LNbits.api.request( + 'PUT', + `/nostrmarket/api/v1/order/restore/${eventId}`, // Restore from DM event + this.adminkey + ) + await this.getOrders() // Refresh order list + this.$q.notify({ + type: 'positive', + message: 'Order restored!' + }) + return data + } catch (error) { + LNbits.utils.notifyApiError(error) + } finally { + this.search.restoring = false + } +} +``` + +#### Bulk Order Restoration +```javascript +restoreOrders: async function () { + try { + this.search.restoring = true + await LNbits.api.request( + 'PUT', + `/nostrmarket/api/v1/orders/restore`, // Restore all from DMs + this.adminkey + ) + await this.getOrders() + this.$q.notify({ + type: 'positive', + message: 'Orders restored!' + }) + } catch (error) { + LNbits.utils.notifyApiError(error) + } +} +``` + +### 5. Invoice Management (`order-list.js:234-258`) + +#### Invoice Reissuance +```javascript +reissueOrderInvoice: async function (order) { + try { + const {data} = await LNbits.api.request( + 'PUT', + `/nostrmarket/api/v1/order/reissue`, + this.adminkey, + { + id: order.id, + shipping_id: order.shipping_id // Optional shipping zone update + } + ) + this.$q.notify({ + type: 'positive', + message: 'Order invoice reissued!' + }) + + // Update order in local state + data.expanded = order.expanded + const i = this.orders.map(o => o.id).indexOf(order.id) + if (i !== -1) { + this.orders[i] = {...this.orders[i], ...data} + } + } catch (error) { + LNbits.utils.notifyApiError(error) + } +} +``` + +## Customer Order Interface + +### 1. Customer Orders Component (`CustomerOrders.vue`) + +#### Order Display Structure +```vue +
+ + + + :pubkey="merchant.pubkey" + :profiles="profiles" + > + + + +
+ + + +
+
+
+
+``` + +### 2. Order Data Enrichment (`CustomerOrders.vue:208-220`) + +#### Order Enhancement Pipeline +```javascript +enrichOrder: function (order) { + const stall = this.stallForOrder(order); + return { + ...order, + stallName: stall?.name || "Stall", // Stall name for display + shippingZone: stall?.shipping?.find( // Shipping zone details + (s) => s.id === order.shipping_id + ) || { id: order.shipping_id, name: order.shipping_id }, + invoice: this.invoiceForOrder(order), // Parsed Lightning invoice + products: this.getProductsForOrder(order), // Product details with quantities + }; +} +``` + +#### Stall Association (`CustomerOrders.vue:221-233`) +```javascript +stallForOrder: function (order) { + try { + const productId = order.items && order.items[0]?.product_id; + if (!productId) return; + + const product = this.products.find((p) => p.id === productId); + if (!product) return; + + const stall = this.stalls.find((s) => s.id === product.stall_id); + if (!stall) return; + + return stall; + } catch (error) { + console.log(error); + } +} +``` + +### 3. Invoice Processing (`CustomerOrders.vue:234-244`) + +#### Lightning Invoice Decoding +```javascript +invoiceForOrder: function (order) { + try { + const lnPaymentOption = order?.payment_options?.find( + (p) => p.type === "ln" // Find Lightning payment option + ); + if (!lnPaymentOption?.link) return; + + return decode(lnPaymentOption.link); // Decode BOLT11 invoice + } catch (error) { + console.warn(error); + } +} +``` + +### 4. Product Aggregation (`CustomerOrders.vue:246-259`) + +#### Order Item Processing +```javascript +getProductsForOrder: function (order) { + if (!order?.items?.length) return []; + + return order.items.map((i) => { + const product = this.products.find((p) => p.id === i.product_id) || { + id: i.product_id, + name: i.product_id, // Fallback if product not found + }; + return { + ...product, + orderedQuantity: i.quantity, // Add ordered quantity to product + }; + }); +} +``` + +## Backend Order Operations + +### 1. Order Creation (`services.py:84-133`) + +#### Order Build Pipeline +```python +async def build_order_with_payment(merchant_id, merchant_public_key, data): + # 1. Validate products and calculate costs + products = await get_products_by_ids(merchant_id, [p.product_id for p in data.items]) + data.validate_order_items(products) # Ensure products exist and have stock + + shipping_zone = await get_zone(merchant_id, data.shipping_id) + product_cost_sat, shipping_cost_sat = await data.costs_in_sats( + products, shipping_zone.id, shipping_zone.cost + ) + + # 2. Check inventory availability + success, _, message = await compute_products_new_quantity( + merchant_id, [i.product_id for i in data.items], data.items + ) + if not success: + raise ValueError(message) # Insufficient inventory + + # 3. Create Lightning invoice via LNbits + payment = await create_invoice( + wallet_id=wallet_id, + amount=round(product_cost_sat + shipping_cost_sat), + memo=f"Order '{data.id}' for pubkey '{data.public_key}'", + extra={ + "tag": "nostrmarket", # Tags invoice as marketplace + "order_id": data.id, + "merchant_pubkey": merchant_public_key, + }, + ) + + # 4. Create order record + order = Order( + **data.dict(), + stall_id=products[0].stall_id, + invoice_id=payment.payment_hash, + total=product_cost_sat + shipping_cost_sat, + extra=extra, + ) + + return order, payment.bolt11, receipt +``` + +### 2. Order Retrieval API (`views_api.py:540-577`) + +#### Multi-filter Order Queries +```python +@nostrmarket_ext.get("/api/v1/stall/order/{stall_id}") +async def api_get_orders_for_stall( + stall_id: str, + paid: Optional[bool] = None, # Filter by payment status + shipped: Optional[bool] = None, # Filter by shipping status + pubkey: Optional[str] = None, # Filter by customer pubkey + wallet: WalletTypeInfo = Depends(require_invoice_key), +) -> List[Order]: + try: + merchant = await get_merchant_for_user(wallet.wallet.user) + assert merchant, "Merchant cannot be found" + + orders = await get_orders_for_stall( + merchant.id, stall_id, paid=paid, shipped=shipped, public_key=pubkey + ) + return orders + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail=str(ex) + ) from ex +``` + +### 3. Order Status Updates (`views_api.py:625-641`) + +#### Shipping Status API +```python +@nostrmarket_ext.patch("/api/v1/order/{order_id}") +async def api_update_order_status( + data: OrderStatusUpdate, + wallet: WalletTypeInfo = Depends(require_admin_key), +) -> Order: + try: + assert data.shipped is not None, "Shipped value is required for order" + merchant = await get_merchant_for_user(wallet.wallet.user) + assert merchant, "Merchant cannot be found for order {data.id}" + + # Update shipping status in database + order = await update_order_shipped_status(merchant.id, data.id, data.shipped) + assert order, "Cannot find updated order" + + # Send status update to customer via DM + data.paid = order.paid # Include current payment status + dm_content = json.dumps( + {"type": DirectMessageType.ORDER_PAID_OR_SHIPPED.value, **data.dict()}, + separators=(",", ":"), + ensure_ascii=False, + ) + await reply_to_structured_dm( + merchant, order.public_key, DirectMessageType.ORDER_PAID_OR_SHIPPED.value, dm_content + ) + + return order + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail=str(ex) + ) from ex +``` + +### 4. Invoice Reissuance (`views_api.py:710-740`) + +#### Payment Request Regeneration +```python +@nostrmarket_ext.put("/api/v1/order/reissue") +async def api_reissue_order_invoice( + reissue_data: OrderReissue, + wallet: WalletTypeInfo = Depends(require_admin_key), +) -> Order: + try: + merchant = await get_merchant_for_user(wallet.wallet.user) + assert merchant, "Merchant cannot be found" + + # Get existing order + data = await get_order(merchant.id, reissue_data.id) + assert data, "Order cannot be found" + + # Update shipping zone if provided + if reissue_data.shipping_id: + data.shipping_id = reissue_data.shipping_id + + # Generate new payment request + payment_req, order = await build_order_with_payment( + merchant.id, merchant.public_key, data + ) + + # Update order with new invoice details + order_update = { + "total": payment_req.total, + "invoice_id": order.invoice_id, # New payment hash + "extra_data": json.dumps(order.extra.dict()), + } + + await update_order( + merchant.id, + order.id, + **order_update, + ) + + # Send new payment request to customer + payment_req = PaymentRequest( + id=order.id, + message="Updated payment request", + payment_options=[PaymentOption(type="ln", link=order.bolt11)], + ) + + dm_content = json.dumps( + {"type": DirectMessageType.PAYMENT_REQUEST.value, **payment_req.dict()}, + ) + await reply_to_structured_dm( + merchant, order.public_key, DirectMessageType.PAYMENT_REQUEST.value, dm_content + ) + + return await get_order(merchant.id, reissue_data.id) + except Exception as ex: + logger.warning(ex) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Cannot reissue order invoice", + ) from ex +``` + +## Order Restoration System + +### 1. Order Recovery from Direct Messages (`services.py:645-690`) + +#### DM-based Order Restoration +```python +async def create_or_update_order_from_dm( + merchant_id: str, merchant_pubkey: str, dm: DirectMessage +): + type_, json_data = PartialDirectMessage.parse_message(dm.message) + if not json_data or "id" not in json_data: + return + + if type_ == DirectMessageType.CUSTOMER_ORDER: + # Restore customer order from DM + order, _ = await extract_customer_order_from_dm( + merchant_id, merchant_pubkey, dm, json_data + ) + new_order = await create_order(merchant_id, order) + + # Handle stall association updates + if new_order.stall_id == "None" and order.stall_id != "None": + await update_order( + merchant_id, + order.id, + **{ + "stall_id": order.stall_id, + "extra_data": json.dumps(order.extra.dict()), + }, + ) + return + + if type_ == DirectMessageType.PAYMENT_REQUEST: + # Update order with payment request details + payment_request = PaymentRequest(**json_data) + pr = payment_request.payment_options[0].link + invoice = decode(pr) + total = invoice.amount_msat / 1000 if invoice.amount_msat else 0 + await update_order( + merchant_id, + payment_request.id, + **{"total": total, "invoice_id": invoice.payment_hash}, + ) + return + + if type_ == DirectMessageType.ORDER_PAID_OR_SHIPPED: + # Update order status from status messages + order_update = OrderStatusUpdate(**json_data) + if order_update.paid: + await update_order_paid_status(order_update.id, True) + if order_update.shipped: + await update_order_shipped_status(merchant_id, order_update.id, True) +``` + +### 2. Bulk Restoration API (`views_api.py:580-595`) + +#### Complete Order Recovery +```python +@nostrmarket_ext.put("/api/v1/orders/restore") +async def api_restore_orders_from_dms( + wallet: WalletTypeInfo = Depends(require_admin_key), +): + try: + merchant = await get_merchant_for_user(wallet.wallet.user) + assert merchant, "Merchant cannot be found" + + # Get all order-related direct messages + dms = await get_orders_from_direct_messages(merchant.id) + for dm in dms: + try: + # Attempt to restore/update each order from DM history + await create_or_update_order_from_dm( + merchant.id, merchant.public_key, dm + ) + except Exception as e: + logger.debug( + f"Failed to restore order from event '{dm.event_id}': '{e!s}'." + ) + continue + + return {"status": "Orders restoration completed!"} + except AssertionError as ex: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail=str(ex) + ) from ex +``` + +## Real-time Order Updates + +### 1. WebSocket Order Notifications (`order-list.js:281-296`) + +#### Live Order Addition +```javascript +addOrder: async function (data) { + if ( + !this.search.publicKey || + this.search.publicKey === data.customerPubkey // Filter matches current view + ) { + const orderData = JSON.parse(data.dm.message) + const i = this.orders.map(o => o.id).indexOf(orderData.id) + if (i === -1) { // Prevent duplicates + const order = await this.getOrder(orderData.id) // Fetch complete order data + this.orders.unshift(order) // Add to top of list + } + } +} +``` + +### 2. Payment Status Updates (`order-list.js:391-396`) + +#### Real-time Payment Confirmation +```javascript +orderPaid: function (orderId) { + const order = this.orders.find(o => o.id === orderId) + if (order) { + order.paid = true // Update payment status immediately + } +} +``` + +## Advanced Order Management Features + +### 1. Order Selection and Deep Linking (`order-list.js:294-315`) + +#### Order Detail Navigation +```javascript +orderSelected: async function (orderId, eventId) { + const order = await this.getOrder(orderId) + if (!order) { + // Order missing - offer restoration from DM + LNbits.utils + .confirmDialog( + 'Order could not be found. Do you want to restore it from this direct message?' + ) + .onOk(async () => { + const restoredOrder = await this.restoreOrder(eventId) + if (restoredOrder) { + restoredOrder.expanded = true + restoredOrder.isNew = false + this.orders = [restoredOrder] // Show only restored order + } + }) + return + } + + // Show order details + order.expanded = true + order.isNew = false + this.orders = [order] // Focus on single order +} +``` + +### 2. Customer Association and Filtering + +#### Customer Management Integration +```javascript +computed: { + customerOptions: function () { + const options = this.customers.map(c => ({ + label: this.buildCustomerLabel(c), // Include unread message counts + value: c.public_key + })) + options.unshift({label: 'All', value: null, id: null}) // All customers option + return options + } +} +``` + +### 3. Shipping Zone Integration (`order-list.js:348-355`) + +#### Dynamic Shipping Options +```javascript +getStallZones: function (stallId) { + const stall = this.stalls.find(s => s.id === stallId) + if (!stall) return [] + + return this.zoneOptions.filter(z => + stall.shipping_zones.find(s => s.id === z.id) // Only zones supported by stall + ) +} +``` + +## Database Operations + +### 1. Order CRUD Operations (`crud.py`) + +#### Order Creation +```python +async def create_order(merchant_id: str, o: Order) -> Order: + await db.execute( + """ + INSERT INTO nostrmarket.orders ( + merchant_id, id, event_id, event_created_at, public_key, + merchant_public_key, contact_data, extra_data, order_items, + address, total, shipping_id, stall_id, invoice_id, paid, shipped + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + merchant_id, o.id, o.event_id, o.event_created_at, o.public_key, + o.merchant_public_key, json.dumps(o.contact.dict()), + json.dumps(o.extra.dict()), json.dumps([i.dict() for i in o.items]), + o.address, o.total, o.shipping_id, o.stall_id, o.invoice_id, + o.paid, o.shipped, + ), + ) + return o +``` + +#### Flexible Order Queries +```python +async def get_orders(merchant_id: str, **kwargs) -> List[Order]: + # Build dynamic WHERE clause from keyword arguments + q = " AND ".join( + [ + f"{field[0]} = :{field[0]}" + for field in kwargs.items() + if field[1] is not None + ] + ) + + rows: list[dict] = await db.fetchall( + f"SELECT * FROM nostrmarket.orders WHERE merchant_id = :merchant_id " + f"{' AND ' + q if q else ''} ORDER BY event_created_at DESC", + {"merchant_id": merchant_id, **kwargs}, + ) + return [Order.from_row(row) for row in rows] +``` + +### 2. Status Update Operations + +#### Payment Status Updates +```python +async def update_order_paid_status(order_id: str, paid: bool) -> Optional[Order]: + await db.execute( + "UPDATE nostrmarket.orders SET paid = :paid WHERE id = :id", + {"paid": paid, "id": order_id}, + ) + row: dict = await db.fetchone( + "SELECT * FROM nostrmarket.orders WHERE id = :id", {"id": order_id} + ) + return Order.from_row(row) if row else None +``` + +#### Shipping Status Updates +```python +async def update_order_shipped_status( + merchant_id: str, order_id: str, shipped: bool +) -> Optional[Order]: + await db.execute( + """ + UPDATE nostrmarket.orders + SET shipped = :shipped + WHERE merchant_id = :merchant_id AND id = :id + """, + {"shipped": shipped, "merchant_id": merchant_id, "id": order_id}, + ) + row: dict = await db.fetchone( + "SELECT * FROM nostrmarket.orders WHERE merchant_id = :merchant_id AND id = :id", + {"merchant_id": merchant_id, "id": order_id}, + ) + return Order.from_row(row) if row else None +``` + +## Error Handling and Edge Cases + +### 1. Order Restoration Failures +- **Missing Products**: Orders reference products that no longer exist +- **Invalid Stall Association**: Product moved between stalls after order creation +- **Corrupted DM Data**: JSON parsing errors in message restoration +- **Payment Hash Conflicts**: Duplicate invoice IDs from reissuance + +### 2. Payment Processing Issues +- **Invoice Expiration**: Lightning invoices expire after timeout +- **Partial Payments**: Underpayment or overpayment scenarios +- **Payment Verification**: Webhook delays or failures +- **Double Payment**: Multiple payments for same order + +### 3. Inventory Synchronization +- **Race Conditions**: Multiple orders for limited stock +- **Negative Inventory**: Orders processed despite insufficient stock +- **Product Updates**: Price or availability changes after order placement +- **Stall Deactivation**: Orders for disabled stalls or products + +## Integration Points + +### 1. Payment System Integration +- **LNbits Invoice Creation**: Automatic Lightning invoice generation +- **Payment Monitoring**: Real-time payment confirmation via webhooks +- **Refund Processing**: Automated refunds for failed orders +- **Multi-currency Support**: Fiat pricing with BTC conversion + +### 2. Inventory Management Integration +- **Stock Validation**: Pre-order inventory checking +- **Automatic Deduction**: Post-payment inventory updates +- **Backorder Handling**: Out-of-stock order management +- **Restock Notifications**: Customer alerts for inventory replenishment + +### 3. Communication System Integration +- **Status Updates**: Automated customer notifications +- **Order Confirmations**: Receipt and tracking information +- **Shipping Notifications**: Fulfillment status updates +- **Support Integration**: Customer service ticket creation + +This comprehensive order management system provides complete lifecycle tracking from initial order placement through final fulfillment, with robust error handling, real-time updates, and flexible merchant tools for efficient order processing and customer communication. \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 649bfd9..9832863 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,10 +6,10 @@ import Footer from '@/components/layout/Footer.vue' import LoginDialog from '@/components/auth/LoginDialog.vue' import { Toaster } from '@/components/ui/sonner' import 'vue-sonner/style.css' -import { auth } from '@/composables/useAuth' import { useMarketPreloader } from '@/composables/useMarketPreloader' import { nostrChat } from '@/composables/useNostrChat' -import { useRelayHub } from '@/composables/useRelayHub' +import { auth } from '@/composables/useAuth' +import { relayHubComposable } from '@/composables/useRelayHub' import { toast } from 'vue-sonner' const route = useRoute() @@ -19,7 +19,7 @@ const showLoginDialog = ref(false) const marketPreloader = useMarketPreloader() // Initialize relay hub -const relayHub = useRelayHub() +const relayHub = relayHubComposable // Hide navbar on login page const showNavbar = computed(() => { diff --git a/src/components/NostrmarketPublisher.vue b/src/components/NostrmarketPublisher.vue new file mode 100644 index 0000000..3740b9b --- /dev/null +++ b/src/components/NostrmarketPublisher.vue @@ -0,0 +1,301 @@ + + + + + diff --git a/src/components/RelayHubStatus.vue b/src/components/RelayHubStatus.vue index 1ee5bb1..8222440 100644 --- a/src/components/RelayHubStatus.vue +++ b/src/components/RelayHubStatus.vue @@ -43,15 +43,12 @@
- -
@@ -65,7 +62,7 @@