Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
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>
This commit is contained in:
353
marketing-agent/services/billing-service/src/routes/invoices.js
Normal file
353
marketing-agent/services/billing-service/src/routes/invoices.js
Normal file
@@ -0,0 +1,353 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user