chore(accounting): PR #622 nits — stray artifact, dedupe requireFlag, IMAP poll backoff
1. Remove the committed test artifact backend/storage/business-docs/quote/2026/ Q-2026-0001.pdf and gitignore backend/storage/business-docs/ so generated CRM docs can't be committed again. 2. adminLedger + adminExpenses dropped their local requireFlag copies and now import the shared (now cached) requireFeatureFlag middleware. 4. roundTripTest polls IMAP with ×1.5 backoff (cap 8s) instead of a flat 3s, so a 30s test takes ~5 SELECT/SEARCH locks not ~10 (some servers throttle). Nit 3 (dashboard + events pages still on the gallery-theme vars, not dark-mode- swapped) is left as a documented follow-up per the review.
This commit is contained in:
@@ -129,3 +129,6 @@ docker-compose.dev.yml
|
|||||||
|
|
||||||
# New layout development files
|
# New layout development files
|
||||||
new-layouts/
|
new-layouts/
|
||||||
|
|
||||||
|
# Generated CRM/accounting documents (runtime) — never commit
|
||||||
|
backend/storage/business-docs/
|
||||||
|
|||||||
@@ -48,19 +48,11 @@ function diskUpload(subdir) {
|
|||||||
const inboundUpload = diskUpload('inbound');
|
const inboundUpload = diskUpload('inbound');
|
||||||
const proofUpload = diskUpload('expenses/proof');
|
const proofUpload = diskUpload('expenses/proof');
|
||||||
|
|
||||||
function requireFlag(key, code) {
|
// Shared cached feature gate (PR #622 nit 2) — replaces the former local copy.
|
||||||
return async (req, res, next) => {
|
const { requireFeatureFlag } = require('../middleware/requireFeatureFlag');
|
||||||
try {
|
const requireIncoming = requireFeatureFlag('incomingInvoices', 'INCOMING_INVOICES_DISABLED');
|
||||||
const row = await db('feature_flags').where({ key }).first();
|
const requireExpenses = requireFeatureFlag('expenses', 'EXPENSES_DISABLED');
|
||||||
const enabled = row && (row.value === true || row.value === 1 || row.value === '1');
|
const requireAccounting = requireFeatureFlag('accounting', 'ACCOUNTING_DISABLED');
|
||||||
if (!enabled) return res.status(403).json({ error: `${key} feature is disabled`, code });
|
|
||||||
return next();
|
|
||||||
} catch (err) { return next(err); }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const requireIncoming = requireFlag('incomingInvoices', 'INCOMING_INVOICES_DISABLED');
|
|
||||||
const requireExpenses = requireFlag('expenses', 'EXPENSES_DISABLED');
|
|
||||||
const requireAccounting = requireFlag('accounting', 'ACCOUNTING_DISABLED');
|
|
||||||
|
|
||||||
router.use(adminAuth);
|
router.use(adminAuth);
|
||||||
|
|
||||||
|
|||||||
@@ -21,17 +21,9 @@ const ledgerService = require('../services/ledgerService');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const toInt = (v) => { const n = parseInt(v, 10); return Number.isFinite(n) ? n : undefined; };
|
const toInt = (v) => { const n = parseInt(v, 10); return Number.isFinite(n) ? n : undefined; };
|
||||||
|
|
||||||
function requireFlag(key, code) {
|
// Shared cached feature gate (PR #622 nit 2) — replaces the former local copy.
|
||||||
return async (req, res, next) => {
|
const { requireFeatureFlag } = require('../middleware/requireFeatureFlag');
|
||||||
try {
|
const requireAccounting = requireFeatureFlag('accounting', 'ACCOUNTING_DISABLED');
|
||||||
const row = await db('feature_flags').where({ key }).first();
|
|
||||||
const enabled = row && (row.value === true || row.value === 1 || row.value === '1');
|
|
||||||
if (!enabled) return res.status(403).json({ error: `${key} feature is disabled`, code });
|
|
||||||
return next();
|
|
||||||
} catch (err) { return next(err); }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const requireAccounting = requireFlag('accounting', 'ACCOUNTING_DISABLED');
|
|
||||||
|
|
||||||
router.use(adminAuth);
|
router.use(adminAuth);
|
||||||
router.use(requireAccounting);
|
router.use(requireAccounting);
|
||||||
|
|||||||
@@ -202,6 +202,9 @@ async function roundTripTest({ timeoutMs = 30000, intervalMs = 3000 } = {}) {
|
|||||||
const client = makeImapClient(cfg);
|
const client = makeImapClient(cfg);
|
||||||
await connectWithTimeout(client);
|
await connectWithTimeout(client);
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
|
// Backoff (PR #622 nit 4): some IMAP servers throttle frequent SELECT/SEARCH.
|
||||||
|
// Grow the gap ×1.5 (cap 8s) so a 30s test does ~5 polls, not ~10.
|
||||||
|
let delay = intervalMs;
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -219,7 +222,8 @@ async function roundTripTest({ timeoutMs = 30000, intervalMs = 3000 } = {}) {
|
|||||||
return { ok: false, sent: true, reason: 'not_received', recipient };
|
return { ok: false, sent: true, reason: 'not_received', recipient };
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await new Promise((r) => setTimeout(r, intervalMs));
|
await new Promise((r) => setTimeout(r, delay));
|
||||||
|
delay = Math.min(Math.round(delay * 1.5), 8000);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await client.logout().catch(() => {});
|
await client.logout().catch(() => {});
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
pdf
|
|
||||||
Reference in New Issue
Block a user