Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
353 lines
7.7 KiB
JavaScript
353 lines
7.7 KiB
JavaScript
import express from 'express';
|
|
import { invoiceService } from '../services/invoiceService.js';
|
|
import { authenticate, requireTenant } from '../middleware/auth.js';
|
|
import { validateRequest } from '../middleware/validation.js';
|
|
import { body, param, query } from 'express-validator';
|
|
import { logger } from '../utils/logger.js';
|
|
|
|
const router = express.Router();
|
|
|
|
// Create invoice
|
|
router.post('/',
|
|
authenticate,
|
|
requireTenant,
|
|
validateRequest([
|
|
body('subscriptionId').optional().isMongoId(),
|
|
body('lineItems').isArray(),
|
|
body('lineItems.*.description').notEmpty(),
|
|
body('lineItems.*.quantity').isInt({ min: 1 }),
|
|
body('lineItems.*.unitPrice').isFloat({ min: 0 }),
|
|
body('dueDate').optional().isISO8601()
|
|
]),
|
|
async (req, res) => {
|
|
try {
|
|
const invoice = await invoiceService.createInvoice(
|
|
req.tenantId,
|
|
{
|
|
...req.body,
|
|
customerId: req.tenant.billing?.customerId,
|
|
customer: {
|
|
name: req.tenant.name,
|
|
email: req.tenant.owner.email,
|
|
address: req.tenant.billing?.billingAddress
|
|
}
|
|
}
|
|
);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
invoice
|
|
});
|
|
} catch (error) {
|
|
logger.error('Create invoice error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to create invoice'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Get invoices
|
|
router.get('/',
|
|
authenticate,
|
|
requireTenant,
|
|
validateRequest([
|
|
query('status').optional().isIn(['draft', 'open', 'paid', 'void', 'uncollectible']),
|
|
query('startDate').optional().isISO8601(),
|
|
query('endDate').optional().isISO8601(),
|
|
query('limit').optional().isInt({ min: 1, max: 100 })
|
|
]),
|
|
async (req, res) => {
|
|
try {
|
|
const invoices = await invoiceService.getInvoices(
|
|
req.tenantId,
|
|
req.query
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
invoices
|
|
});
|
|
} catch (error) {
|
|
logger.error('Get invoices error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to get invoices'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Get unpaid invoices
|
|
router.get('/unpaid',
|
|
authenticate,
|
|
requireTenant,
|
|
async (req, res) => {
|
|
try {
|
|
const invoices = await invoiceService.getUnpaidInvoices(req.tenantId);
|
|
|
|
res.json({
|
|
success: true,
|
|
invoices,
|
|
totalDue: invoices.reduce((sum, inv) => sum + inv.amountDue, 0)
|
|
});
|
|
} catch (error) {
|
|
logger.error('Get unpaid invoices error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to get unpaid invoices'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Get overdue invoices
|
|
router.get('/overdue',
|
|
authenticate,
|
|
requireTenant,
|
|
async (req, res) => {
|
|
try {
|
|
const invoices = await invoiceService.getOverdueInvoices(req.tenantId);
|
|
|
|
res.json({
|
|
success: true,
|
|
invoices,
|
|
totalOverdue: invoices.reduce((sum, inv) => sum + inv.amountDue, 0)
|
|
});
|
|
} catch (error) {
|
|
logger.error('Get overdue invoices error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to get overdue invoices'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Get invoice by ID
|
|
router.get('/:id',
|
|
authenticate,
|
|
requireTenant,
|
|
validateRequest([
|
|
param('id').isMongoId()
|
|
]),
|
|
async (req, res) => {
|
|
try {
|
|
const invoice = await invoiceService.getInvoice(
|
|
req.tenantId,
|
|
req.params.id
|
|
);
|
|
|
|
if (!invoice) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Invoice not found'
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
invoice
|
|
});
|
|
} catch (error) {
|
|
logger.error('Get invoice error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to get invoice'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Update invoice (draft only)
|
|
router.patch('/:id',
|
|
authenticate,
|
|
requireTenant,
|
|
validateRequest([
|
|
param('id').isMongoId(),
|
|
body('lineItems').optional().isArray(),
|
|
body('dueDate').optional().isISO8601()
|
|
]),
|
|
async (req, res) => {
|
|
try {
|
|
const invoice = await invoiceService.updateInvoice(
|
|
req.tenantId,
|
|
req.params.id,
|
|
req.body
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
invoice
|
|
});
|
|
} catch (error) {
|
|
logger.error('Update invoice error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to update invoice'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Finalize invoice
|
|
router.post('/:id/finalize',
|
|
authenticate,
|
|
requireTenant,
|
|
validateRequest([
|
|
param('id').isMongoId()
|
|
]),
|
|
async (req, res) => {
|
|
try {
|
|
const invoice = await invoiceService.finalizeInvoice(
|
|
req.tenantId,
|
|
req.params.id
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
invoice,
|
|
message: 'Invoice finalized and sent'
|
|
});
|
|
} catch (error) {
|
|
logger.error('Finalize invoice error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to finalize invoice'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Pay invoice
|
|
router.post('/:id/pay',
|
|
authenticate,
|
|
requireTenant,
|
|
validateRequest([
|
|
param('id').isMongoId(),
|
|
body('paymentMethodId').optional().isString()
|
|
]),
|
|
async (req, res) => {
|
|
try {
|
|
const invoice = await invoiceService.payInvoice(
|
|
req.tenantId,
|
|
req.params.id,
|
|
{
|
|
paymentMethodId: req.body.paymentMethodId
|
|
}
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
invoice,
|
|
message: 'Invoice paid successfully'
|
|
});
|
|
} catch (error) {
|
|
logger.error('Pay invoice error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to pay invoice'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Void invoice
|
|
router.post('/:id/void',
|
|
authenticate,
|
|
requireTenant,
|
|
validateRequest([
|
|
param('id').isMongoId(),
|
|
body('reason').notEmpty()
|
|
]),
|
|
async (req, res) => {
|
|
try {
|
|
const invoice = await invoiceService.voidInvoice(
|
|
req.tenantId,
|
|
req.params.id,
|
|
req.body.reason
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
invoice,
|
|
message: 'Invoice voided'
|
|
});
|
|
} catch (error) {
|
|
logger.error('Void invoice error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to void invoice'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Download invoice PDF
|
|
router.get('/:id/pdf',
|
|
authenticate,
|
|
requireTenant,
|
|
validateRequest([
|
|
param('id').isMongoId()
|
|
]),
|
|
async (req, res) => {
|
|
try {
|
|
const invoice = await invoiceService.getInvoice(
|
|
req.tenantId,
|
|
req.params.id
|
|
);
|
|
|
|
if (!invoice) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Invoice not found'
|
|
});
|
|
}
|
|
|
|
// Generate PDF if not exists
|
|
if (!invoice.pdfUrl) {
|
|
await invoiceService.generatePDF(invoice);
|
|
}
|
|
|
|
// In production, redirect to cloud storage URL
|
|
res.redirect(invoice.pdfUrl);
|
|
} catch (error) {
|
|
logger.error('Download invoice PDF error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to download invoice'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// Send invoice reminder
|
|
router.post('/:id/remind',
|
|
authenticate,
|
|
requireTenant,
|
|
validateRequest([
|
|
param('id').isMongoId()
|
|
]),
|
|
async (req, res) => {
|
|
try {
|
|
await invoiceService.sendInvoiceReminder(
|
|
req.tenantId,
|
|
req.params.id
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Invoice reminder sent'
|
|
});
|
|
} catch (error) {
|
|
logger.error('Send reminder error:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to send reminder'
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
export default router; |