Files
antonym/server/routes/webhooks.ts
Dorian 54500a68e6 feat: scaffold Antonym fashion store
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>
2026-03-17 00:23:21 +00:00

48 lines
3.0 KiB
TypeScript

import { Router } from 'express'
import { getDb } from '../db/connection.js'
import { validateWebhookSignature } from '../lib/btcpay.js'
import { sendOrderConfirmation } from '../lib/mailer.js'
import { notifyOrderConfirmed } from '../lib/nostr.js'
import type { SizeStock } from '../../shared/types.js'
export const webhooksRouter = Router()
interface OrderRow { id: string; nostr_pubkey: string | null; email: string | null; items: string; total_sats: number; status: string }
webhooksRouter.post('/btcpay', async (req, res) => {
try {
const signature = req.headers['btcpay-sig'] as string | undefined
const rawBody = JSON.stringify(req.body)
if (!signature || !validateWebhookSignature(rawBody, signature)) { res.status(401).json({ error: { code: 'INVALID_SIGNATURE', message: 'Invalid webhook signature' } }); return }
const { type, invoiceId } = req.body as { type: string; invoiceId: string }
const db = getDb()
const order = db.prepare('SELECT * FROM orders WHERE btcpay_invoice_id = ?').get(invoiceId) as OrderRow | undefined
if (!order) { res.status(404).json({ error: { code: 'ORDER_NOT_FOUND', message: 'Order not found for invoice' } }); return }
if (type === 'InvoiceSettled' || type === 'InvoicePaymentSettled') {
if (order.status !== 'pending') { res.json({ ok: true, message: 'Already processed' }); return }
const items = JSON.parse(order.items) as { productId: string; size: string; quantity: number }[]
const decrementStock = db.transaction(() => {
for (const item of items) {
const product = db.prepare('SELECT sizes FROM products WHERE id = ?').get(item.productId) as { sizes: string } | undefined
if (!product) continue
const sizes = JSON.parse(product.sizes) as SizeStock[]
const sizeEntry = sizes.find((s) => s.size === item.size)
if (sizeEntry) sizeEntry.stock = Math.max(0, sizeEntry.stock - item.quantity)
db.prepare("UPDATE products SET sizes = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(sizes), item.productId)
}
db.prepare("UPDATE orders SET status = ?, updated_at = datetime('now') WHERE id = ?").run('paid', order.id)
db.prepare('INSERT INTO order_events (order_id, status, note) VALUES (?, ?, ?)').run(order.id, 'paid', 'Payment confirmed via BTCPay')
})
decrementStock()
if (order.email) sendOrderConfirmation(order.email, order.id, order.total_sats).catch(() => {})
if (order.nostr_pubkey) notifyOrderConfirmed(order.nostr_pubkey, order.id, order.total_sats).catch(() => {})
} else if (type === 'InvoiceExpired') {
db.prepare("UPDATE orders SET status = ?, updated_at = datetime('now') WHERE id = ?").run('cancelled', order.id)
db.prepare('INSERT INTO order_events (order_id, status, note) VALUES (?, ?, ?)').run(order.id, 'cancelled', 'Invoice expired')
}
res.json({ ok: true })
} catch (err) { console.error('Webhook processing failed:', err); res.status(500).json({ error: { code: 'WEBHOOK_FAILED', message: 'Webhook processing failed' } }) }
})