better states
This commit is contained in:
@@ -135,7 +135,23 @@ router.post('/', async (req, res) => {
|
||||
return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` });
|
||||
}
|
||||
}
|
||||
// Validate items
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(400).json({ error: 'Please add at least one item.' });
|
||||
}
|
||||
|
||||
// Recurring invoices must contain at least one Subscription item
|
||||
if (is_recurring) {
|
||||
const subscriptionItems = items.filter(item => String(item.qbo_item_id) === '115');
|
||||
|
||||
if (subscriptionItems.length === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(400).json({
|
||||
error: 'Recurring invoices must contain at least one Subscription item.'
|
||||
});
|
||||
}
|
||||
}
|
||||
let subtotal = 0;
|
||||
for (const item of items) {
|
||||
const amount = parseFloat(item.amount.replace(/[$,]/g, ''));
|
||||
@@ -210,7 +226,23 @@ router.put('/:id', async (req, res) => {
|
||||
return res.status(400).json({ error: `Invoice number ${invoice_number} already exists.` });
|
||||
}
|
||||
}
|
||||
// Validate items
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(400).json({ error: 'Please add at least one item.' });
|
||||
}
|
||||
|
||||
// Recurring invoices must contain at least one Subscription item
|
||||
if (is_recurring) {
|
||||
const subscriptionItems = items.filter(item => String(item.qbo_item_id) === '115');
|
||||
|
||||
if (subscriptionItems.length === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(400).json({
|
||||
error: 'Recurring invoices must contain at least one Subscription item.'
|
||||
});
|
||||
}
|
||||
}
|
||||
let subtotal = 0;
|
||||
for (const item of items) {
|
||||
const amount = parseFloat(item.amount.replace(/[$,]/g, ''));
|
||||
|
||||
@@ -36,13 +36,13 @@ async function processRecurringInvoices() {
|
||||
console.log(`🔄 [RECURRING] Checking for due recurring invoices (today: ${today})...`);
|
||||
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// Find all recurring invoices that are due
|
||||
const dueResult = await client.query(`
|
||||
SELECT i.*, c.qbo_id as customer_qbo_id, c.name as customer_name
|
||||
FROM invoices i
|
||||
LEFT JOIN customers c ON i.customer_id = c.id
|
||||
WHERE i.is_recurring = true
|
||||
WHERE i.is_recurring = true
|
||||
AND i.next_recurring_date IS NOT NULL
|
||||
AND i.next_recurring_date <= $1
|
||||
`, [today]);
|
||||
@@ -55,38 +55,86 @@ async function processRecurringInvoices() {
|
||||
console.log(`🔄 [RECURRING] Found ${dueResult.rows.length} recurring invoice(s) due.`);
|
||||
|
||||
let created = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const source of dueResult.rows) {
|
||||
await client.query('BEGIN');
|
||||
|
||||
try {
|
||||
// Load items from the source invoice
|
||||
const itemsResult = await client.query(
|
||||
'SELECT * FROM invoice_items WHERE invoice_id = $1 ORDER BY item_order',
|
||||
`
|
||||
SELECT *
|
||||
FROM invoice_items
|
||||
WHERE invoice_id = $1
|
||||
AND qbo_item_id::text = '115'
|
||||
ORDER BY item_order
|
||||
`,
|
||||
[source.id]
|
||||
);
|
||||
|
||||
const subscriptionItems = itemsResult.rows;
|
||||
|
||||
const nextDate = advanceDate(source.next_recurring_date, source.recurring_interval);
|
||||
|
||||
if (subscriptionItems.length === 0) {
|
||||
console.warn(
|
||||
` ⚠️ Recurring invoice #${source.invoice_number || source.id} has no Subscription items. No new invoice created.`
|
||||
);
|
||||
|
||||
await client.query(
|
||||
'UPDATE invoices SET next_recurring_date = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
|
||||
[nextDate, source.id]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newInvoiceDate = source.next_recurring_date.toISOString().split('T')[0];
|
||||
|
||||
// Create the new invoice (no invoice_number — QBO will assign one)
|
||||
let subtotal = 0;
|
||||
for (const item of subscriptionItems) {
|
||||
const amount = parseFloat(String(item.amount || '0').replace(/[$,]/g, ''));
|
||||
if (!Number.isNaN(amount)) {
|
||||
subtotal += amount;
|
||||
}
|
||||
}
|
||||
|
||||
const taxRate = parseFloat(source.tax_rate) || 8.25;
|
||||
const taxAmount = source.tax_exempt ? 0 : (subtotal * taxRate / 100);
|
||||
const total = subtotal + taxAmount;
|
||||
|
||||
const newInvoice = await client.query(
|
||||
`INSERT INTO invoices (
|
||||
invoice_number, customer_id, invoice_date, terms, auth_code,
|
||||
tax_exempt, tax_rate, subtotal, tax_amount, total,
|
||||
bill_to_name, recurring_source_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
RETURNING *`,
|
||||
`
|
||||
INSERT INTO invoices (
|
||||
invoice_number,
|
||||
customer_id,
|
||||
invoice_date,
|
||||
terms,
|
||||
auth_code,
|
||||
tax_exempt,
|
||||
tax_rate,
|
||||
subtotal,
|
||||
tax_amount,
|
||||
total,
|
||||
bill_to_name,
|
||||
recurring_source_id
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
RETURNING *
|
||||
`,
|
||||
[
|
||||
`DRAFT-${Date.now()}`, // Temporary, QBO export will assign real number
|
||||
`DRAFT-${Date.now()}`,
|
||||
source.customer_id,
|
||||
newInvoiceDate,
|
||||
source.terms,
|
||||
source.auth_code,
|
||||
source.tax_exempt,
|
||||
source.tax_rate,
|
||||
source.subtotal,
|
||||
source.tax_amount,
|
||||
source.total,
|
||||
taxRate,
|
||||
subtotal,
|
||||
taxAmount,
|
||||
total,
|
||||
source.bill_to_name,
|
||||
source.id
|
||||
]
|
||||
@@ -94,18 +142,34 @@ async function processRecurringInvoices() {
|
||||
|
||||
const newInvoiceId = newInvoice.rows[0].id;
|
||||
|
||||
// Copy items
|
||||
for (let i = 0; i < itemsResult.rows.length; i++) {
|
||||
const item = itemsResult.rows[i];
|
||||
for (let i = 0; i < subscriptionItems.length; i++) {
|
||||
const item = subscriptionItems[i];
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO invoice_items (invoice_id, quantity, description, rate, amount, item_order, qbo_item_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[newInvoiceId, item.quantity, item.description, item.rate, item.amount, i, item.qbo_item_id || '9']
|
||||
`
|
||||
INSERT INTO invoice_items (
|
||||
invoice_id,
|
||||
quantity,
|
||||
description,
|
||||
rate,
|
||||
amount,
|
||||
item_order,
|
||||
qbo_item_id
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`,
|
||||
[
|
||||
newInvoiceId,
|
||||
item.quantity,
|
||||
item.description,
|
||||
item.rate,
|
||||
item.amount,
|
||||
i,
|
||||
item.qbo_item_id || '115'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Advance the source invoice's next_recurring_date
|
||||
const nextDate = advanceDate(source.next_recurring_date, source.recurring_interval);
|
||||
await client.query(
|
||||
'UPDATE invoices SET next_recurring_date = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2',
|
||||
[nextDate, source.id]
|
||||
@@ -113,13 +177,16 @@ async function processRecurringInvoices() {
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
console.log(` ✅ Created recurring invoice from #${source.invoice_number || source.id} → new ID ${newInvoiceId} (date: ${newInvoiceDate}), next due: ${nextDate}`);
|
||||
console.log(
|
||||
` ✅ Created recurring invoice from #${source.invoice_number || source.id} → new ID ${newInvoiceId} (date: ${newInvoiceDate}), next due: ${nextDate}`
|
||||
);
|
||||
|
||||
// Auto-export to QBO (outside transaction, non-blocking)
|
||||
try {
|
||||
const dbClient = await pool.connect();
|
||||
|
||||
try {
|
||||
const qboResult = await exportInvoiceToQbo(newInvoiceId, dbClient);
|
||||
|
||||
if (qboResult.success) {
|
||||
console.log(` 📤 Auto-exported to QBO: #${qboResult.qbo_doc_number}`);
|
||||
} else if (qboResult.skipped) {
|
||||
@@ -129,18 +196,24 @@ async function processRecurringInvoices() {
|
||||
dbClient.release();
|
||||
}
|
||||
} catch (qboErr) {
|
||||
console.error(` ⚠️ QBO auto-export failed for recurring invoice ${newInvoiceId}:`, qboErr.message);
|
||||
console.error(
|
||||
` ⚠️ QBO auto-export failed for recurring invoice ${newInvoiceId}:`,
|
||||
qboErr.message
|
||||
);
|
||||
}
|
||||
|
||||
created++;
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error(` ❌ Failed to create recurring invoice from #${source.invoice_number || source.id}:`, err.message);
|
||||
console.error(
|
||||
` ❌ Failed to create recurring invoice from #${source.invoice_number || source.id}:`,
|
||||
err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔄 [RECURRING] Done. Created ${created} invoice(s).`);
|
||||
return { created };
|
||||
console.log(`🔄 [RECURRING] Done. Created ${created} invoice(s), skipped ${skipped}.`);
|
||||
return { created, skipped };
|
||||
} catch (error) {
|
||||
console.error('❌ [RECURRING] Error:', error.message);
|
||||
return { created: 0, error: error.message };
|
||||
|
||||
Reference in New Issue
Block a user