Anonymous Bitcoin-only fashion e-commerce with: - Vue 3 + Tailwind 4 frontend with glassmorphism dark/light design system - Express 5 + SQLite backend with BTCPay Server integration - Nostr identity (NIP-07/keypair) for anonymous purchase tracking - ChaCha20-Poly1305 encrypted shipping addresses - Admin panel with order/product/stock management - SVG logo splash animation with clip-path reveal - 5 seeded products across 4 categories Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
57 lines
3.9 KiB
TypeScript
57 lines
3.9 KiB
TypeScript
import { Router } from 'express'
|
|
import { nanoid } from 'nanoid'
|
|
import { getDb } from '../db/connection.js'
|
|
import { encrypt } from '../lib/crypto.js'
|
|
import { createInvoice } from '../lib/btcpay.js'
|
|
import { requireBody, sanitizeString } from '../middleware/validate.js'
|
|
import type { CreateOrderRequest, Order, OrderItem, OrderEvent, OrderStatus } from '../../shared/types.js'
|
|
|
|
export const ordersRouter = Router()
|
|
|
|
interface OrderRow { id: string; nostr_pubkey: string | null; email: string | null; btcpay_invoice_id: string | null; status: string; shipping_address_encrypted: string | null; items: string; total_sats: number; note: string | null; created_at: string; updated_at: string }
|
|
|
|
function rowToOrder(row: OrderRow): Order {
|
|
return { id: row.id, nostrPubkey: row.nostr_pubkey, email: row.email, btcpayInvoiceId: row.btcpay_invoice_id, status: row.status as OrderStatus, items: JSON.parse(row.items) as OrderItem[], totalSats: row.total_sats, note: row.note, createdAt: row.created_at, updatedAt: row.updated_at }
|
|
}
|
|
|
|
ordersRouter.post('/', requireBody('items', 'shippingAddress'), async (req, res) => {
|
|
try {
|
|
const body = req.body as CreateOrderRequest
|
|
const db = getDb()
|
|
if (!Array.isArray(body.items) || body.items.length === 0) { res.status(400).json({ error: { code: 'INVALID_ITEMS', message: 'Order must contain items' } }); return }
|
|
|
|
const orderItems: OrderItem[] = []
|
|
let totalSats = 0
|
|
|
|
for (const item of body.items) {
|
|
const product = db.prepare('SELECT * FROM products WHERE id = ? AND is_active = 1').get(item.productId) as { name: string; price_sats: number; sizes: string } | undefined
|
|
if (!product) { res.status(400).json({ error: { code: 'INVALID_PRODUCT', message: `Product ${item.productId} not found` } }); return }
|
|
const sizes = JSON.parse(product.sizes) as { size: string; stock: number }[]
|
|
const sizeEntry = sizes.find((s) => s.size === item.size)
|
|
if (!sizeEntry || sizeEntry.stock < item.quantity) { res.status(400).json({ error: { code: 'INSUFFICIENT_STOCK', message: `Insufficient stock for ${product.name} size ${item.size}` } }); return }
|
|
const lineTotal = product.price_sats * item.quantity
|
|
totalSats += lineTotal
|
|
orderItems.push({ productId: item.productId, productName: product.name, size: item.size, quantity: item.quantity, priceSats: product.price_sats })
|
|
}
|
|
|
|
const orderId = nanoid()
|
|
const encryptedAddress = encrypt(JSON.stringify(body.shippingAddress))
|
|
const siteUrl = process.env.SITE_URL || 'http://localhost:3333'
|
|
const invoice = await createInvoice({ amountSats: totalSats, orderId, redirectUrl: `${siteUrl}/order/${orderId}` })
|
|
|
|
db.prepare('INSERT INTO orders (id, nostr_pubkey, email, btcpay_invoice_id, status, shipping_address_encrypted, items, total_sats, note) VALUES (?, ?, ?, ?, \'pending\', ?, ?, ?, ?)').run(orderId, body.nostrPubkey ? sanitizeString(body.nostrPubkey) : null, body.email ? sanitizeString(body.email) : null, invoice.id, encryptedAddress, JSON.stringify(orderItems), totalSats, body.note ? sanitizeString(body.note) : null)
|
|
db.prepare('INSERT INTO order_events (order_id, status, note) VALUES (?, ?, ?)').run(orderId, 'pending', 'Order created')
|
|
|
|
res.status(201).json({ orderId, invoiceUrl: invoice.checkoutLink, invoiceId: invoice.id })
|
|
} catch (err) { console.error('Order creation failed:', err); res.status(500).json({ error: { code: 'ORDER_FAILED', message: 'Failed to create order' } }) }
|
|
})
|
|
|
|
ordersRouter.get('/:id', (req, res) => {
|
|
const db = getDb()
|
|
const row = db.prepare('SELECT * FROM orders WHERE id = ?').get(req.params.id) as OrderRow | undefined
|
|
if (!row) { res.status(404).json({ error: { code: 'NOT_FOUND', message: 'Order not found' } }); return }
|
|
const order = rowToOrder(row)
|
|
const events = db.prepare('SELECT * FROM order_events WHERE order_id = ? ORDER BY created_at ASC').all(req.params.id) as OrderEvent[]
|
|
res.json({ ...order, events })
|
|
})
|