Create comprehensive Obsidian-style documentation structure
- Reorganize all markdown documentation into structured docs/ folder - Create 7 main documentation categories (00-overview through 06-deployment) - Add comprehensive index files for each category with cross-linking - Implement Obsidian-compatible [[link]] syntax throughout - Move legacy/deprecated documentation to archive folder - Establish documentation standards and maintenance guidelines - Provide complete coverage of modular architecture, services, and deployment - Enable better navigation and discoverability for developers and contributors 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
46856134ef
commit
cdf099e45f
29 changed files with 3733 additions and 0 deletions
187
docs/02-modules/chat-module/integration.md
Normal file
187
docs/02-modules/chat-module/integration.md
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
# Nostr Chat Integration for Web-App
|
||||
|
||||
This document describes the Nostr chat integration that allows LNBits users to chat with each other using Nostr relays.
|
||||
|
||||
## Overview
|
||||
|
||||
The chat system integrates with the LNBits user system and Nostr relays to provide encrypted messaging between users. Each user has a Nostr keypair (stored in `pubkey` and `prvkey` fields) that enables secure communication.
|
||||
|
||||
## Navigation Integration
|
||||
|
||||
The chat feature is accessible through the main navigation menu:
|
||||
- **Desktop**: Chat link appears in the top navigation bar with a message icon
|
||||
- **Mobile**: Chat link appears in the mobile menu with a message icon
|
||||
- **Route**: `/chat` - Accessible to authenticated users only
|
||||
|
||||
## Components
|
||||
|
||||
### 1. ChatComponent.vue
|
||||
**Location**: `src/components/nostr/ChatComponent.vue`
|
||||
|
||||
A Vue component that provides the chat interface with:
|
||||
- Peer list populated from LNBits users
|
||||
- Real-time messaging using Nostr relays
|
||||
- Encrypted message exchange
|
||||
- Connection status indicators
|
||||
|
||||
### 2. useNostrChat.ts
|
||||
**Location**: `src/composables/useNostrChat.ts`
|
||||
|
||||
A composable that handles:
|
||||
- Nostr relay connections
|
||||
- Message encryption/decryption
|
||||
- User authentication with LNBits
|
||||
- Real-time message subscription
|
||||
|
||||
### 3. ChatPage.vue
|
||||
**Location**: `src/pages/ChatPage.vue`
|
||||
|
||||
A page that integrates the chat component into the web-app.
|
||||
|
||||
### 4. Navigation Integration
|
||||
**Location**: `src/components/layout/Navbar.vue`
|
||||
|
||||
The chat link has been added to the main navigation with:
|
||||
- Message icon for visual identification
|
||||
- Internationalization support (English, Spanish, French)
|
||||
- Responsive design for desktop and mobile
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Get Current User
|
||||
```bash
|
||||
GET /users/api/v1/user/me
|
||||
Authorization: Bearer <admin_token>
|
||||
|
||||
Response:
|
||||
{
|
||||
"id": "user_id",
|
||||
"username": "username",
|
||||
"email": "email@example.com",
|
||||
"pubkey": "nostr_public_key",
|
||||
"prvkey": "nostr_private_key_hex",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Get All User Public Keys
|
||||
```bash
|
||||
GET /users/api/v1/nostr/pubkeys
|
||||
Authorization: Bearer <admin_token>
|
||||
|
||||
Response:
|
||||
[
|
||||
{
|
||||
"user_id": "user_id",
|
||||
"username": "username",
|
||||
"pubkey": "nostr_public_key"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### 1. User Integration
|
||||
- Automatically loads peers from LNBits user database
|
||||
- Uses existing `pubkey` and `prvkey` fields
|
||||
- Admin-only access to private keys for messaging
|
||||
|
||||
### 2. Nostr Relay Integration
|
||||
- Connects to multiple Nostr relays for redundancy
|
||||
- Real-time message delivery
|
||||
- Encrypted end-to-end messaging
|
||||
|
||||
### 3. UI Features
|
||||
- Peer list with user avatars and names
|
||||
- Real-time message display
|
||||
- Connection status indicators
|
||||
- Message timestamps
|
||||
- Auto-scroll to latest messages
|
||||
|
||||
### 4. Mobile-Responsive Design
|
||||
- **Mobile-first approach**: Optimized for touch interactions
|
||||
- **Peer list view**: Shows only peers list on mobile until a peer is selected
|
||||
- **Full-width chat view**: When a peer is selected, switches to full-width chat
|
||||
- **Back button**: Easy navigation back to peers list
|
||||
- **Touch-friendly**: Larger touch targets and proper touch feedback
|
||||
- **Responsive avatars**: Larger avatars on mobile for better visibility
|
||||
- **Message bubbles**: Optimized width (75% max) for mobile readability
|
||||
- **Keyboard-friendly**: Input stays visible when keyboard appears
|
||||
|
||||
### 5. Navigation Features
|
||||
- Integrated into main navigation menu
|
||||
- Message icon for easy identification
|
||||
- Multi-language support
|
||||
- Responsive design for all devices
|
||||
|
||||
## Security
|
||||
|
||||
1. **Encryption**: All messages are encrypted using NIP-04 (Nostr encrypted direct messages)
|
||||
2. **Private Key Access**: Only admin users can access private keys for messaging
|
||||
3. **Relay Security**: Messages are distributed across multiple relays for redundancy
|
||||
4. **User Authentication**: Requires LNBits authentication to access chat
|
||||
|
||||
## Setup Requirements
|
||||
|
||||
1. **NostrTools**: The web-app needs NostrTools loaded globally
|
||||
2. **Admin Access**: Users need admin privileges to access private keys
|
||||
3. **Relay Configuration**: Default relays are configured in the composable
|
||||
4. **LNBits Integration**: Requires the updated LNBits API endpoints
|
||||
|
||||
## Usage
|
||||
|
||||
1. Navigate to `/chat` in the web-app (or click "Chat" in the navigation)
|
||||
2. The system will automatically load peers from LNBits
|
||||
3. Select a peer to start chatting
|
||||
4. Messages are encrypted and sent via Nostr relays
|
||||
|
||||
## Configuration
|
||||
|
||||
### Default Relays
|
||||
The system connects to these relays by default:
|
||||
- `wss://nostr.atitlan.io`
|
||||
- `wss://relay.damus.io`
|
||||
- `wss://nos.lol`
|
||||
|
||||
### Relay Configuration
|
||||
You can modify the relays in `useNostrChat.ts`:
|
||||
```typescript
|
||||
const DEFAULT_RELAYS: NostrRelayConfig[] = [
|
||||
{ url: 'wss://your-relay.com', read: true, write: true }
|
||||
]
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Message Persistence**: Store messages locally for offline access
|
||||
2. **File Sharing**: Support for encrypted file sharing
|
||||
3. **Group Chats**: Multi-user encrypted conversations
|
||||
4. **Message Search**: Search through conversation history
|
||||
5. **Push Notifications**: Real-time notifications for new messages
|
||||
6. **Profile Integration**: Display user profiles and avatars
|
||||
7. **Message Reactions**: Support for message reactions and replies
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Connection Failed**: Check relay availability and network connectivity
|
||||
2. **Messages Not Sending**: Verify user has admin privileges and private key access
|
||||
3. **Peers Not Loading**: Check LNBits API endpoint and authentication
|
||||
4. **Encryption Errors**: Ensure NostrTools is properly loaded
|
||||
|
||||
### Debug Information
|
||||
|
||||
The chat component logs detailed information to the console:
|
||||
- Connection status
|
||||
- Message encryption/decryption
|
||||
- Relay connection attempts
|
||||
- API call results
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **NostrTools**: For Nostr protocol implementation
|
||||
- **Vue 3**: For reactive UI components
|
||||
- **LNBits API**: For user management and authentication
|
||||
- **Nostr Relays**: For message distribution
|
||||
386
docs/02-modules/index.md
Normal file
386
docs/02-modules/index.md
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
# 📦 Module System Overview
|
||||
|
||||
> **Modular architecture** enabling feature-based development with plugin-based modules, dependency injection, and clean separation of concerns.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [[#Module Architecture]]
|
||||
- [[#Available Modules]]
|
||||
- [[#Module Development]]
|
||||
- [[#Module Configuration]]
|
||||
- [[#Inter-Module Communication]]
|
||||
- [[#Module Lifecycle]]
|
||||
|
||||
## Module Architecture
|
||||
|
||||
### **Plugin-Based Design**
|
||||
Ario uses a plugin-based architecture where each feature is implemented as a self-contained module:
|
||||
|
||||
- **Independent Development** - Modules can be developed, tested, and deployed separately
|
||||
- **Optional Features** - Modules can be enabled or disabled via configuration
|
||||
- **Clear Boundaries** - Each module owns its domain logic and UI components
|
||||
- **Extensible** - New modules can be added without modifying existing code
|
||||
|
||||
### **Module Structure**
|
||||
Each module follows a consistent directory structure:
|
||||
```
|
||||
src/modules/[module-name]/
|
||||
├── index.ts # Module plugin definition
|
||||
├── components/ # Module-specific UI components
|
||||
├── composables/ # Module composables and hooks
|
||||
├── services/ # Business logic and API services
|
||||
├── stores/ # Module-specific Pinia stores
|
||||
├── types/ # TypeScript type definitions
|
||||
└── views/ # Page components and routes
|
||||
```
|
||||
|
||||
### **Module Plugin Interface**
|
||||
All modules implement the `ModulePlugin` interface:
|
||||
```typescript
|
||||
interface ModulePlugin {
|
||||
name: string // Unique module identifier
|
||||
version: string // Semantic version
|
||||
dependencies: string[] // Required module dependencies
|
||||
|
||||
// Installation lifecycle
|
||||
install(app: App, options?: ModuleConfig): Promise<void>
|
||||
|
||||
// Optional exports
|
||||
routes?: RouteRecordRaw[] // Vue Router routes
|
||||
components?: Record<string, any> // Global components
|
||||
composables?: Record<string, any> // Exported composables
|
||||
}
|
||||
```
|
||||
|
||||
## Available Modules
|
||||
|
||||
### **Base Module** 🏗️
|
||||
**Purpose:** Core infrastructure and shared services
|
||||
**Location:** `src/modules/base/`
|
||||
**Dependencies:** None (foundation module)
|
||||
|
||||
**Provides:**
|
||||
- **Authentication Service** - User identity management and Nostr key handling
|
||||
- **Relay Hub** - Centralized Nostr relay connection management
|
||||
- **Storage Service** - User-scoped localStorage operations
|
||||
- **Toast Service** - Application-wide notifications and feedback
|
||||
- **PWA Features** - Service worker and offline capabilities
|
||||
|
||||
**Key Components:**
|
||||
- Identity management UI (key generation, import/export)
|
||||
- Connection status indicators
|
||||
- Theme and language switching
|
||||
- Authentication guards and utilities
|
||||
|
||||
**See:** [[base-module/index|📖 Base Module Documentation]]
|
||||
|
||||
### **Nostr Feed Module** 📰
|
||||
**Purpose:** Social feed and content discovery
|
||||
**Location:** `src/modules/nostr-feed/`
|
||||
**Dependencies:** `['base']`
|
||||
|
||||
**Features:**
|
||||
- **Social Feed** - Timeline of Nostr events (kind 1 notes)
|
||||
- **Admin Announcements** - Highlighted posts from configured admin pubkeys
|
||||
- **Content Filtering** - Filter by author, content type, or keywords
|
||||
- **Real-time Updates** - Live feed updates via Nostr subscriptions
|
||||
- **Engagement** - Like, repost, and reply to posts
|
||||
|
||||
**Key Components:**
|
||||
- FeedComponent with infinite scroll
|
||||
- NoteCard for individual posts
|
||||
- AdminBadge for announcement highlighting
|
||||
- Content filtering and search
|
||||
|
||||
**See:** [[nostr-feed-module/index|📖 Nostr Feed Documentation]]
|
||||
|
||||
### **Chat Module** 💬
|
||||
**Purpose:** Encrypted direct messaging
|
||||
**Location:** `src/modules/chat/`
|
||||
**Dependencies:** `['base']`
|
||||
|
||||
**Features:**
|
||||
- **Encrypted Messages** - NIP-04 encrypted direct messages
|
||||
- **Contact Management** - Add and manage chat contacts
|
||||
- **Real-time Chat** - Live message delivery via Nostr relays
|
||||
- **Message History** - Persistent chat history with local storage
|
||||
- **Typing Indicators** - Real-time typing status (when supported)
|
||||
|
||||
**Key Components:**
|
||||
- ChatComponent with message bubbles
|
||||
- ContactList for chat participants
|
||||
- MessageInput with encryption handling
|
||||
- Chat history management
|
||||
|
||||
**See:** [[chat-module/index|📖 Chat Module Documentation]]
|
||||
|
||||
### **Events Module** 🎟️
|
||||
**Purpose:** Event ticketing and management
|
||||
**Location:** `src/modules/events/`
|
||||
**Dependencies:** `['base']`
|
||||
|
||||
**Features:**
|
||||
- **Event Creation** - Create and publish events to Nostr
|
||||
- **Lightning Tickets** - Paid event tickets using Lightning invoices
|
||||
- **Event Discovery** - Browse and search upcoming events
|
||||
- **Ticket Management** - Purchase, transfer, and validate tickets
|
||||
- **Event Check-in** - QR code-based event entry system
|
||||
|
||||
**Key Components:**
|
||||
- EventCard for event display
|
||||
- TicketPurchase with Lightning payment flow
|
||||
- EventCreation form with rich editing
|
||||
- QR code generation and scanning
|
||||
|
||||
**See:** [[events-module/index|📖 Events Module Documentation]]
|
||||
|
||||
### **Market Module** 🛒
|
||||
**Purpose:** Nostr marketplace functionality
|
||||
**Location:** `src/modules/market/`
|
||||
**Dependencies:** `['base']`
|
||||
|
||||
**Features:**
|
||||
- **Product Listings** - Create and browse marketplace items
|
||||
- **Lightning Payments** - Bitcoin payments for products
|
||||
- **Vendor Profiles** - Seller reputation and product history
|
||||
- **Order Management** - Track purchases and sales
|
||||
- **Product Search** - Filter and search marketplace items
|
||||
|
||||
**Key Components:**
|
||||
- ProductCard for item display
|
||||
- ProductListing creation form
|
||||
- OrderHistory and transaction tracking
|
||||
- Vendor dashboard and analytics
|
||||
|
||||
**See:** [[market-module/index|📖 Market Module Documentation]]
|
||||
|
||||
## Module Development
|
||||
|
||||
### **Creating a New Module**
|
||||
|
||||
#### 1. Module Structure Setup
|
||||
```bash
|
||||
mkdir src/modules/my-module
|
||||
cd src/modules/my-module
|
||||
|
||||
# Create module directories
|
||||
mkdir components composables services stores types views
|
||||
touch index.ts
|
||||
```
|
||||
|
||||
#### 2. Module Plugin Definition
|
||||
```typescript
|
||||
// src/modules/my-module/index.ts
|
||||
import type { App } from 'vue'
|
||||
import type { ModulePlugin } from '@/core/types'
|
||||
|
||||
export const myModule: ModulePlugin = {
|
||||
name: 'my-module',
|
||||
version: '1.0.0',
|
||||
dependencies: ['base'], // Always depend on base for core services
|
||||
|
||||
async install(app: App, options?: MyModuleConfig) {
|
||||
// Register module components
|
||||
// Initialize module services
|
||||
// Set up module routes
|
||||
},
|
||||
|
||||
routes: [
|
||||
{
|
||||
path: '/my-feature',
|
||||
component: () => import('./views/MyFeatureView.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Service Implementation
|
||||
```typescript
|
||||
// src/modules/my-module/services/my-service.ts
|
||||
import { BaseService } from '@/core/base/BaseService'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
|
||||
export class MyService extends BaseService {
|
||||
constructor(
|
||||
private relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
// Service initialization logic
|
||||
this.isInitialized.value = true
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
// Cleanup logic
|
||||
this.isDisposed.value = true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Module Registration
|
||||
```typescript
|
||||
// src/app.config.ts
|
||||
export const moduleConfigs = {
|
||||
// ... existing modules
|
||||
'my-module': {
|
||||
enabled: true,
|
||||
customOption: 'value'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **Module Development Best Practices**
|
||||
|
||||
#### **Dependency Management**
|
||||
- Always declare module dependencies explicitly
|
||||
- Use dependency injection for cross-module service access
|
||||
- Avoid direct imports between modules
|
||||
|
||||
#### **Service Architecture**
|
||||
- Extend `BaseService` for consistent lifecycle management
|
||||
- Register services in the DI container during module installation
|
||||
- Use reactive properties for state that affects UI
|
||||
|
||||
#### **Component Patterns**
|
||||
- Export reusable components for other modules
|
||||
- Use module-specific component naming (e.g., `ChatMessage`, `EventCard`)
|
||||
- Follow the existing UI component patterns with Shadcn/ui
|
||||
|
||||
## Module Configuration
|
||||
|
||||
### **Configuration Schema**
|
||||
Modules can be configured via `src/app.config.ts`:
|
||||
```typescript
|
||||
interface ModuleConfig {
|
||||
enabled: boolean // Enable/disable module
|
||||
[key: string]: any // Module-specific configuration
|
||||
}
|
||||
|
||||
export const moduleConfigs: Record<string, ModuleConfig> = {
|
||||
'base': { enabled: true },
|
||||
'chat': {
|
||||
enabled: true,
|
||||
maxMessageLength: 1000,
|
||||
enableTypingIndicators: true
|
||||
},
|
||||
'events': {
|
||||
enabled: true,
|
||||
defaultCurrency: 'sat',
|
||||
allowedEventTypes: ['meetup', 'conference', 'workshop']
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **Runtime Configuration**
|
||||
Configuration is passed to modules during installation:
|
||||
```typescript
|
||||
async install(app: App, options?: ChatModuleConfig) {
|
||||
const config = options || defaultConfig
|
||||
|
||||
// Use configuration to customize module behavior
|
||||
this.messageService.setMaxLength(config.maxMessageLength)
|
||||
}
|
||||
```
|
||||
|
||||
## Inter-Module Communication
|
||||
|
||||
### **Service Dependencies**
|
||||
For required functionality between modules:
|
||||
```typescript
|
||||
// ✅ Correct: Use dependency injection
|
||||
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
||||
|
||||
// ❌ Wrong: Direct import breaks modularity
|
||||
import { relayHub } from '../base/services/relay-hub'
|
||||
```
|
||||
|
||||
### **Event Bus Communication**
|
||||
For optional cross-module notifications:
|
||||
```typescript
|
||||
// Publishing events
|
||||
eventBus.emit('user:authenticated', { userId: user.pubkey })
|
||||
eventBus.emit('payment:received', { amount: 1000, invoiceId: 'abc123' })
|
||||
|
||||
// Subscribing to events
|
||||
eventBus.on('chat:message-received', (message) => {
|
||||
// Handle message in events module
|
||||
})
|
||||
```
|
||||
|
||||
### **Shared Components**
|
||||
Modules can export components for use by other modules:
|
||||
```typescript
|
||||
// In module plugin definition
|
||||
export const chatModule: ModulePlugin = {
|
||||
// ...
|
||||
components: {
|
||||
'ChatAvatar': () => import('./components/ChatAvatar.vue'),
|
||||
'MessageBubble': () => import('./components/MessageBubble.vue')
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in other modules
|
||||
<ChatAvatar :pubkey="user.pubkey" :size="32" />
|
||||
```
|
||||
|
||||
## Module Lifecycle
|
||||
|
||||
### **Initialization Order**
|
||||
1. **Dependency Resolution** - PluginManager sorts modules by dependencies
|
||||
2. **Service Registration** - Modules register services in DI container
|
||||
3. **Component Registration** - Global components made available
|
||||
4. **Route Registration** - Module routes added to Vue Router
|
||||
5. **Service Initialization** - Services initialize in dependency order
|
||||
|
||||
### **Module Installation Process**
|
||||
```typescript
|
||||
async install(app: App, options?: ModuleConfig) {
|
||||
// 1. Register services
|
||||
container.provide(SERVICE_TOKENS.MY_SERVICE, new MyService())
|
||||
|
||||
// 2. Register global components
|
||||
app.component('MyGlobalComponent', MyGlobalComponent)
|
||||
|
||||
// 3. Initialize module-specific logic
|
||||
await this.initializeModule(options)
|
||||
}
|
||||
```
|
||||
|
||||
### **Service Lifecycle Management**
|
||||
```typescript
|
||||
// Service initialization (called automatically)
|
||||
async initialize(): Promise<void> {
|
||||
await this.setupEventListeners()
|
||||
await this.loadUserData()
|
||||
this.isInitialized.value = true
|
||||
}
|
||||
|
||||
// Service disposal (called on app unmount)
|
||||
async dispose(): Promise<void> {
|
||||
this.removeEventListeners()
|
||||
await this.saveUserData()
|
||||
this.isDisposed.value = true
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
### Module Documentation
|
||||
- **[[base-module/index|🏗️ Base Module]]** - Core infrastructure services
|
||||
- **[[chat-module/index|💬 Chat Module]]** - Encrypted messaging system
|
||||
- **[[events-module/index|🎟️ Events Module]]** - Lightning event ticketing
|
||||
- **[[market-module/index|🛒 Market Module]]** - Nostr marketplace
|
||||
- **[[nostr-feed-module/index|📰 Nostr Feed]]** - Social feed functionality
|
||||
|
||||
### Architecture References
|
||||
- **[[../01-architecture/modular-design|🔧 Modular Design Patterns]]** - Architecture principles
|
||||
- **[[../01-architecture/dependency-injection|⚙️ Dependency Injection]]** - Service container system
|
||||
- **[[../04-development/index|💻 Development Guide]]** - Module development workflows
|
||||
|
||||
---
|
||||
|
||||
**Tags:** #modules #architecture #plugin-system #development
|
||||
**Last Updated:** 2025-09-06
|
||||
**Author:** Development Team
|
||||
921
docs/02-modules/market-module/order-management.md
Normal file
921
docs/02-modules/market-module/order-management.md
Normal file
|
|
@ -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
|
||||
<div v-for="merchant in merchantOrders" :key="merchant.id">
|
||||
<q-card bordered class="q-mb-md">
|
||||
<q-item>
|
||||
<user-profile <!-- Merchant identity -->
|
||||
:pubkey="merchant.pubkey"
|
||||
:profiles="profiles"
|
||||
></user-profile>
|
||||
</q-item>
|
||||
|
||||
<q-list>
|
||||
<div v-for="order in merchant.orders" :key="order.id">
|
||||
<q-expansion-item dense expand-separator>
|
||||
<template v-slot:header>
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<strong><span v-text="order.stallName"></span></strong>
|
||||
<q-badge <!-- Total amount -->
|
||||
v-if="order.invoice?.human_readable_part?.amount"
|
||||
color="orange"
|
||||
>
|
||||
<span v-text="formatCurrency(order.invoice.human_readable_part.amount / 1000, 'sat')"></span>
|
||||
</q-badge>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-badge :color="order.paid ? 'green' : 'grey'"> <!-- Payment status -->
|
||||
<span v-text="order.paid ? 'Paid' : 'Not Paid'"></span>
|
||||
</q-badge>
|
||||
<q-badge :color="order.shipped ? 'green' : 'grey'"> <!-- Shipping status -->
|
||||
<span v-text="order.shipped ? 'Shipped' : 'Not Shipped'"></span>
|
||||
</q-badge>
|
||||
</q-item-section>
|
||||
</template>
|
||||
</q-expansion-item>
|
||||
</div>
|
||||
</q-list>
|
||||
</q-card>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue