Bug & tweak sprint: fix financial calculations, add quarterly report, enhance dashboard
- Fix Accounts page: include investment accounts in Est. Monthly Interest calc, add Fund column to investment table, split summary cards into Operating/Reserve - Fix Cash Flow: ending balance now respects includeInvestments toggle - Fix Budget Manager: separate operating/reserve income in summary cards - Fix Projects: default sort by planned_date instead of name - Add Vendors: last_negotiated date field with migration, CSV import/export - New Quarterly Financial Report: budget vs actuals, over-budget flagging, YTD - Enhance Dashboard: separate Operating/Reserve fund cards, expanded Quick Stats with monthly interest, YTD interest earned, planned capital spend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -202,6 +202,7 @@ export class TenantSchemaService {
|
||||
default_account_id UUID REFERENCES "${s}".accounts(id),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
ytd_payments DECIMAL(15,2) DEFAULT 0.00,
|
||||
last_negotiated DATE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)`,
|
||||
|
||||
@@ -7,7 +7,7 @@ export class ProjectsService {
|
||||
|
||||
async findAll() {
|
||||
const projects = await this.tenant.query(
|
||||
'SELECT * FROM projects WHERE is_active = true ORDER BY name',
|
||||
'SELECT * FROM projects WHERE is_active = true ORDER BY planned_date NULLS LAST, target_year NULLS LAST, target_month NULLS LAST, name',
|
||||
);
|
||||
return this.computeFunding(projects);
|
||||
}
|
||||
|
||||
@@ -66,4 +66,20 @@ export class ReportsController {
|
||||
const mo = Math.min(parseInt(months || '') || 24, 48);
|
||||
return this.reportsService.getCashFlowForecast(yr, mo);
|
||||
}
|
||||
|
||||
@Get('quarterly')
|
||||
getQuarterlyFinancial(
|
||||
@Query('year') year?: string,
|
||||
@Query('quarter') quarter?: string,
|
||||
) {
|
||||
const now = new Date();
|
||||
const defaultYear = now.getFullYear();
|
||||
// Default to last complete quarter
|
||||
const currentQuarter = Math.ceil((now.getMonth() + 1) / 3);
|
||||
const defaultQuarter = currentQuarter > 1 ? currentQuarter - 1 : 4;
|
||||
const defaultQYear = currentQuarter > 1 ? defaultYear : defaultYear - 1;
|
||||
const yr = parseInt(year || '') || defaultQYear;
|
||||
const q = Math.min(Math.max(parseInt(quarter || '') || defaultQuarter, 1), 4);
|
||||
return this.reportsService.getQuarterlyFinancial(yr, q);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,8 @@ export class ReportsService {
|
||||
const totalOperating = operatingItems.reduce((s: number, r: any) => s + r.amount, 0);
|
||||
const totalReserve = reserveItems.reduce((s: number, r: any) => s + r.amount, 0);
|
||||
const beginningBalance = parseFloat(beginCash[0]?.balance || '0') + (includeInvestments ? investmentBalance : 0);
|
||||
const endingBalance = parseFloat(endCash[0]?.balance || '0') + investmentBalance;
|
||||
// Only include investment balances in ending balance when includeInvestments is toggled on
|
||||
const endingBalance = parseFloat(endCash[0]?.balance || '0') + (includeInvestments ? investmentBalance : 0);
|
||||
|
||||
return {
|
||||
from, to,
|
||||
@@ -444,24 +445,43 @@ export class ReportsService {
|
||||
}
|
||||
|
||||
async getDashboardKPIs() {
|
||||
// Total cash: ALL asset accounts (not just those named "Cash")
|
||||
// Uses proper double-entry balance: debit - credit for assets
|
||||
const cash = await this.tenant.query(`
|
||||
// Operating cash (asset accounts, fund_type=operating)
|
||||
const opCash = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(sub.balance), 0) as total FROM (
|
||||
SELECT COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0) as balance
|
||||
FROM accounts a
|
||||
LEFT JOIN journal_entry_lines jel ON jel.account_id = a.id
|
||||
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_id AND je.is_posted = true AND je.is_void = false
|
||||
WHERE a.account_type = 'asset' AND a.is_active = true
|
||||
WHERE a.account_type = 'asset' AND a.fund_type = 'operating' AND a.is_active = true
|
||||
GROUP BY a.id
|
||||
) sub
|
||||
`);
|
||||
// Also include investment account current_value in total cash
|
||||
const investmentCash = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(current_value), 0) as total
|
||||
FROM investment_accounts WHERE is_active = true
|
||||
// Reserve cash (asset accounts, fund_type=reserve)
|
||||
const resCash = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(sub.balance), 0) as total FROM (
|
||||
SELECT COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0) as balance
|
||||
FROM accounts a
|
||||
LEFT JOIN journal_entry_lines jel ON jel.account_id = a.id
|
||||
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_id AND je.is_posted = true AND je.is_void = false
|
||||
WHERE a.account_type = 'asset' AND a.fund_type = 'reserve' AND a.is_active = true
|
||||
GROUP BY a.id
|
||||
) sub
|
||||
`);
|
||||
const totalCash = parseFloat(cash[0]?.total || '0') + parseFloat(investmentCash[0]?.total || '0');
|
||||
// Investment accounts split by fund type
|
||||
const opInv = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(current_value), 0) as total
|
||||
FROM investment_accounts WHERE fund_type = 'operating' AND is_active = true
|
||||
`);
|
||||
const resInv = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(current_value), 0) as total
|
||||
FROM investment_accounts WHERE fund_type = 'reserve' AND is_active = true
|
||||
`);
|
||||
|
||||
const operatingCash = parseFloat(opCash[0]?.total || '0');
|
||||
const reserveCash = parseFloat(resCash[0]?.total || '0');
|
||||
const operatingInvestments = parseFloat(opInv[0]?.total || '0');
|
||||
const reserveInvestments = parseFloat(resInv[0]?.total || '0');
|
||||
const totalCash = operatingCash + reserveCash + operatingInvestments + reserveInvestments;
|
||||
|
||||
// Receivables: sum of unpaid invoices
|
||||
const ar = await this.tenant.query(`
|
||||
@@ -469,9 +489,7 @@ export class ReportsService {
|
||||
FROM invoices WHERE status NOT IN ('paid', 'void', 'written_off')
|
||||
`);
|
||||
|
||||
// Reserve fund balance: use the reserve equity accounts (fund balance accounts like 3100)
|
||||
// The equity accounts track the total reserve fund position via double-entry bookkeeping
|
||||
// This is the standard HOA approach — every reserve contribution/expenditure flows through equity
|
||||
// Reserve fund balance via equity accounts + reserve investments
|
||||
const reserves = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(sub.balance), 0) as total FROM (
|
||||
SELECT COALESCE(SUM(jel.credit), 0) - COALESCE(SUM(jel.debit), 0) as balance
|
||||
@@ -482,17 +500,43 @@ export class ReportsService {
|
||||
GROUP BY a.id
|
||||
) sub
|
||||
`);
|
||||
// Add reserve investment account values to the reserve fund total
|
||||
const reserveInvestments = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(current_value), 0) as total
|
||||
FROM investment_accounts WHERE fund_type = 'reserve' AND is_active = true
|
||||
`);
|
||||
|
||||
// Delinquent count (overdue invoices)
|
||||
const delinquent = await this.tenant.query(`
|
||||
SELECT COUNT(DISTINCT unit_id) as count FROM invoices WHERE status = 'overdue'
|
||||
`);
|
||||
|
||||
// Monthly interest estimate from accounts + investments with rates
|
||||
const acctInterest = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(
|
||||
(COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0)) * (a.interest_rate / 100) / 12
|
||||
), 0) as total
|
||||
FROM accounts a
|
||||
LEFT JOIN journal_entry_lines jel ON jel.account_id = a.id
|
||||
LEFT JOIN journal_entries je ON je.id = jel.journal_entry_id AND je.is_posted = true AND je.is_void = false
|
||||
WHERE a.account_type = 'asset' AND a.is_active = true AND a.interest_rate > 0
|
||||
GROUP BY a.id
|
||||
`);
|
||||
const acctInterestTotal = (acctInterest || []).reduce((s: number, r: any) => s + parseFloat(r.total || '0'), 0);
|
||||
const invInterest = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(current_value * interest_rate / 100 / 12), 0) as total
|
||||
FROM investment_accounts WHERE is_active = true AND interest_rate > 0
|
||||
`);
|
||||
const estMonthlyInterest = acctInterestTotal + parseFloat(invInterest[0]?.total || '0');
|
||||
|
||||
// Interest earned YTD from investment accounts
|
||||
const interestEarned = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(interest_earned), 0) as total
|
||||
FROM investment_accounts WHERE is_active = true
|
||||
`);
|
||||
|
||||
// Planned capital spend for current year
|
||||
const currentYear = new Date().getFullYear();
|
||||
const capitalSpend = await this.tenant.query(`
|
||||
SELECT COALESCE(SUM(estimated_cost), 0) as total
|
||||
FROM projects WHERE target_year = $1 AND status IN ('planned', 'in_progress') AND is_active = true
|
||||
`, [currentYear]);
|
||||
|
||||
// Recent transactions
|
||||
const recentTx = await this.tenant.query(`
|
||||
SELECT je.id, je.entry_date, je.description, je.entry_type,
|
||||
@@ -504,9 +548,17 @@ export class ReportsService {
|
||||
return {
|
||||
total_cash: totalCash.toFixed(2),
|
||||
total_receivables: ar[0]?.total || '0.00',
|
||||
reserve_fund_balance: (parseFloat(reserves[0]?.total || '0') + parseFloat(reserveInvestments[0]?.total || '0')).toFixed(2),
|
||||
reserve_fund_balance: (parseFloat(reserves[0]?.total || '0') + reserveInvestments).toFixed(2),
|
||||
delinquent_units: parseInt(delinquent[0]?.count || '0'),
|
||||
recent_transactions: recentTx,
|
||||
// Enhanced split data
|
||||
operating_cash: operatingCash.toFixed(2),
|
||||
reserve_cash: reserveCash.toFixed(2),
|
||||
operating_investments: operatingInvestments.toFixed(2),
|
||||
reserve_investments: reserveInvestments.toFixed(2),
|
||||
est_monthly_interest: estMonthlyInterest.toFixed(2),
|
||||
interest_earned_ytd: interestEarned[0]?.total || '0.00',
|
||||
planned_capital_spend: capitalSpend[0]?.total || '0.00',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -795,4 +847,168 @@ export class ReportsService {
|
||||
datapoints,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Quarterly Financial Report: quarter income statement, YTD income statement,
|
||||
* budget vs actuals for the quarter and YTD, and over-budget items.
|
||||
*/
|
||||
async getQuarterlyFinancial(year: number, quarter: number) {
|
||||
// Quarter date ranges
|
||||
const qStartMonths = [1, 4, 7, 10];
|
||||
const qEndMonths = [3, 6, 9, 12];
|
||||
const qStart = `${year}-${String(qStartMonths[quarter - 1]).padStart(2, '0')}-01`;
|
||||
const qEndMonth = qEndMonths[quarter - 1];
|
||||
const qEndDay = [31, 30, 30, 31][quarter - 1]; // Mar=31, Jun=30, Sep=30, Dec=31
|
||||
const qEnd = `${year}-${String(qEndMonth).padStart(2, '0')}-${qEndDay}`;
|
||||
const ytdStart = `${year}-01-01`;
|
||||
|
||||
// Quarter and YTD income statements (reuse existing method)
|
||||
const quarterIS = await this.getIncomeStatement(qStart, qEnd);
|
||||
const ytdIS = await this.getIncomeStatement(ytdStart, qEnd);
|
||||
|
||||
// Budget data for the quarter months
|
||||
const budgetMonthCols = {
|
||||
1: ['jan', 'feb', 'mar'],
|
||||
2: ['apr', 'may', 'jun'],
|
||||
3: ['jul', 'aug', 'sep'],
|
||||
4: ['oct', 'nov', 'dec_amt'],
|
||||
} as Record<number, string[]>;
|
||||
const ytdMonthCols = {
|
||||
1: ['jan', 'feb', 'mar'],
|
||||
2: ['jan', 'feb', 'mar', 'apr', 'may', 'jun'],
|
||||
3: ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep'],
|
||||
4: ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec_amt'],
|
||||
} as Record<number, string[]>;
|
||||
|
||||
const qCols = budgetMonthCols[quarter];
|
||||
const ytdCols = ytdMonthCols[quarter];
|
||||
|
||||
const budgetRows = await this.tenant.query(
|
||||
`SELECT b.account_id, a.account_number, a.name, a.account_type, a.fund_type,
|
||||
b.jan, b.feb, b.mar, b.apr, b.may, b.jun,
|
||||
b.jul, b.aug, b.sep, b.oct, b.nov, b.dec_amt
|
||||
FROM budgets b
|
||||
JOIN accounts a ON a.id = b.account_id
|
||||
WHERE b.fiscal_year = $1`, [year],
|
||||
);
|
||||
|
||||
// Actual amounts per account for the quarter and YTD
|
||||
const quarterActuals = await this.tenant.query(`
|
||||
SELECT a.id as account_id, a.account_number, a.name, a.account_type, a.fund_type,
|
||||
CASE
|
||||
WHEN a.account_type = 'income'
|
||||
THEN COALESCE(SUM(jel.credit), 0) - COALESCE(SUM(jel.debit), 0)
|
||||
ELSE COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0)
|
||||
END as amount
|
||||
FROM accounts a
|
||||
JOIN journal_entry_lines jel ON jel.account_id = a.id
|
||||
JOIN journal_entries je ON je.id = jel.journal_entry_id
|
||||
AND je.is_posted = true AND je.is_void = false
|
||||
AND je.entry_date BETWEEN $1 AND $2
|
||||
WHERE a.account_type IN ('income', 'expense') AND a.is_active = true
|
||||
GROUP BY a.id, a.account_number, a.name, a.account_type, a.fund_type
|
||||
`, [qStart, qEnd]);
|
||||
|
||||
const ytdActuals = await this.tenant.query(`
|
||||
SELECT a.id as account_id, a.account_number, a.name, a.account_type, a.fund_type,
|
||||
CASE
|
||||
WHEN a.account_type = 'income'
|
||||
THEN COALESCE(SUM(jel.credit), 0) - COALESCE(SUM(jel.debit), 0)
|
||||
ELSE COALESCE(SUM(jel.debit), 0) - COALESCE(SUM(jel.credit), 0)
|
||||
END as amount
|
||||
FROM accounts a
|
||||
JOIN journal_entry_lines jel ON jel.account_id = a.id
|
||||
JOIN journal_entries je ON je.id = jel.journal_entry_id
|
||||
AND je.is_posted = true AND je.is_void = false
|
||||
AND je.entry_date BETWEEN $1 AND $2
|
||||
WHERE a.account_type IN ('income', 'expense') AND a.is_active = true
|
||||
GROUP BY a.id, a.account_number, a.name, a.account_type, a.fund_type
|
||||
`, [ytdStart, qEnd]);
|
||||
|
||||
// Build budget vs actual comparison
|
||||
const actualsByIdQ = new Map<string, number>();
|
||||
for (const a of quarterActuals) {
|
||||
actualsByIdQ.set(a.account_id, parseFloat(a.amount) || 0);
|
||||
}
|
||||
const actualsByIdYTD = new Map<string, number>();
|
||||
for (const a of ytdActuals) {
|
||||
actualsByIdYTD.set(a.account_id, parseFloat(a.amount) || 0);
|
||||
}
|
||||
|
||||
const budgetVsActual: any[] = [];
|
||||
const overBudgetItems: any[] = [];
|
||||
|
||||
for (const b of budgetRows) {
|
||||
const qBudget = qCols.reduce((sum: number, col: string) => sum + (parseFloat(b[col]) || 0), 0);
|
||||
const ytdBudget = ytdCols.reduce((sum: number, col: string) => sum + (parseFloat(b[col]) || 0), 0);
|
||||
const qActual = actualsByIdQ.get(b.account_id) || 0;
|
||||
const ytdActual = actualsByIdYTD.get(b.account_id) || 0;
|
||||
|
||||
if (qBudget === 0 && ytdBudget === 0 && qActual === 0 && ytdActual === 0) continue;
|
||||
|
||||
const qVariance = qActual - qBudget;
|
||||
const ytdVariance = ytdActual - ytdBudget;
|
||||
const isExpense = b.account_type === 'expense';
|
||||
|
||||
const item = {
|
||||
account_id: b.account_id,
|
||||
account_number: b.account_number,
|
||||
name: b.name,
|
||||
account_type: b.account_type,
|
||||
fund_type: b.fund_type,
|
||||
quarter_budget: qBudget,
|
||||
quarter_actual: qActual,
|
||||
quarter_variance: qVariance,
|
||||
ytd_budget: ytdBudget,
|
||||
ytd_actual: ytdActual,
|
||||
ytd_variance: ytdVariance,
|
||||
};
|
||||
budgetVsActual.push(item);
|
||||
|
||||
// Flag expenses over budget by more than 10%
|
||||
if (isExpense && qBudget > 0 && qActual > qBudget * 1.1) {
|
||||
overBudgetItems.push({
|
||||
...item,
|
||||
variance_pct: ((qActual / qBudget - 1) * 100).toFixed(1),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Also include accounts with actuals but no budget
|
||||
for (const a of quarterActuals) {
|
||||
if (!budgetRows.find((b: any) => b.account_id === a.account_id)) {
|
||||
const ytdActual = actualsByIdYTD.get(a.account_id) || 0;
|
||||
budgetVsActual.push({
|
||||
account_id: a.account_id,
|
||||
account_number: a.account_number,
|
||||
name: a.name,
|
||||
account_type: a.account_type,
|
||||
fund_type: a.fund_type,
|
||||
quarter_budget: 0,
|
||||
quarter_actual: parseFloat(a.amount) || 0,
|
||||
quarter_variance: parseFloat(a.amount) || 0,
|
||||
ytd_budget: 0,
|
||||
ytd_actual: ytdActual,
|
||||
ytd_variance: ytdActual,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: income first, then expenses, both by account number
|
||||
budgetVsActual.sort((a: any, b: any) => {
|
||||
if (a.account_type !== b.account_type) return a.account_type === 'income' ? -1 : 1;
|
||||
return (a.account_number || '').localeCompare(b.account_number || '');
|
||||
});
|
||||
|
||||
return {
|
||||
year,
|
||||
quarter,
|
||||
quarter_label: `Q${quarter} ${year}`,
|
||||
date_range: { from: qStart, to: qEnd },
|
||||
quarter_income_statement: quarterIS,
|
||||
ytd_income_statement: ytdIS,
|
||||
budget_vs_actual: budgetVsActual,
|
||||
over_budget_items: overBudgetItems,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
27
backend/src/modules/vendors/vendors.service.ts
vendored
27
backend/src/modules/vendors/vendors.service.ts
vendored
@@ -17,10 +17,10 @@ export class VendorsService {
|
||||
|
||||
async create(dto: any) {
|
||||
const rows = await this.tenant.query(
|
||||
`INSERT INTO vendors (name, contact_name, email, phone, address_line1, city, state, zip_code, tax_id, is_1099_eligible, default_account_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *`,
|
||||
`INSERT INTO vendors (name, contact_name, email, phone, address_line1, city, state, zip_code, tax_id, is_1099_eligible, default_account_id, last_negotiated)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *`,
|
||||
[dto.name, dto.contact_name, dto.email, dto.phone, dto.address_line1, dto.city, dto.state, dto.zip_code,
|
||||
dto.tax_id, dto.is_1099_eligible || false, dto.default_account_id || null],
|
||||
dto.tax_id, dto.is_1099_eligible || false, dto.default_account_id || null, dto.last_negotiated || null],
|
||||
);
|
||||
return rows[0];
|
||||
}
|
||||
@@ -32,24 +32,25 @@ export class VendorsService {
|
||||
email = COALESCE($4, email), phone = COALESCE($5, phone), address_line1 = COALESCE($6, address_line1),
|
||||
city = COALESCE($7, city), state = COALESCE($8, state), zip_code = COALESCE($9, zip_code),
|
||||
tax_id = COALESCE($10, tax_id), is_1099_eligible = COALESCE($11, is_1099_eligible),
|
||||
default_account_id = COALESCE($12, default_account_id), updated_at = NOW()
|
||||
default_account_id = COALESCE($12, default_account_id), last_negotiated = $13, updated_at = NOW()
|
||||
WHERE id = $1 RETURNING *`,
|
||||
[id, dto.name, dto.contact_name, dto.email, dto.phone, dto.address_line1, dto.city, dto.state,
|
||||
dto.zip_code, dto.tax_id, dto.is_1099_eligible, dto.default_account_id],
|
||||
dto.zip_code, dto.tax_id, dto.is_1099_eligible, dto.default_account_id, dto.last_negotiated || null],
|
||||
);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async exportCSV(): Promise<string> {
|
||||
const rows = await this.tenant.query(
|
||||
`SELECT name, contact_name, email, phone, address_line1, city, state, zip_code, tax_id, is_1099_eligible
|
||||
`SELECT name, contact_name, email, phone, address_line1, city, state, zip_code, tax_id, is_1099_eligible, last_negotiated
|
||||
FROM vendors WHERE is_active = true ORDER BY name`,
|
||||
);
|
||||
const headers = ['name', 'contact_name', 'email', 'phone', 'address_line1', 'city', 'state', 'zip_code', 'tax_id', 'is_1099_eligible'];
|
||||
const headers = ['name', 'contact_name', 'email', 'phone', 'address_line1', 'city', 'state', 'zip_code', 'tax_id', 'is_1099_eligible', 'last_negotiated'];
|
||||
const lines = [headers.join(',')];
|
||||
for (const r of rows) {
|
||||
lines.push(headers.map((h) => {
|
||||
const v = r[h] ?? '';
|
||||
let v = r[h] ?? '';
|
||||
if (v instanceof Date) v = v.toISOString().split('T')[0];
|
||||
const s = String(v);
|
||||
return s.includes(',') || s.includes('"') ? `"${s.replace(/"/g, '""')}"` : s;
|
||||
}).join(','));
|
||||
@@ -80,20 +81,22 @@ export class VendorsService {
|
||||
zip_code = COALESCE(NULLIF($8, ''), zip_code),
|
||||
tax_id = COALESCE(NULLIF($9, ''), tax_id),
|
||||
is_1099_eligible = COALESCE(NULLIF($10, '')::boolean, is_1099_eligible),
|
||||
last_negotiated = COALESCE(NULLIF($11, '')::date, last_negotiated),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`,
|
||||
[existing[0].id, row.contact_name, row.email, row.phone, row.address_line1,
|
||||
row.city, row.state, row.zip_code, row.tax_id, row.is_1099_eligible],
|
||||
row.city, row.state, row.zip_code, row.tax_id, row.is_1099_eligible, row.last_negotiated],
|
||||
);
|
||||
updated++;
|
||||
} else {
|
||||
await this.tenant.query(
|
||||
`INSERT INTO vendors (name, contact_name, email, phone, address_line1, city, state, zip_code, tax_id, is_1099_eligible)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
`INSERT INTO vendors (name, contact_name, email, phone, address_line1, city, state, zip_code, tax_id, is_1099_eligible, last_negotiated)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
|
||||
[name, row.contact_name || null, row.email || null, row.phone || null,
|
||||
row.address_line1 || null, row.city || null, row.state || null,
|
||||
row.zip_code || null, row.tax_id || null,
|
||||
row.is_1099_eligible === 'true' || row.is_1099_eligible === true || false],
|
||||
row.is_1099_eligible === 'true' || row.is_1099_eligible === true || false,
|
||||
row.last_negotiated || null],
|
||||
);
|
||||
created++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user