fix: conform moved code to eslint indent/quotes, 4-arg mutation callbacks

- eslint --fix on branch-changed backend files (indent shift from the
  module-wrapper nesting in decomposed files); backend lint now 904
  errors vs 1,315 on main
- useMutationWithToast forwards all four TanStack v5 callback args
  (tsc -b strict build flagged the 3-arg passthrough)
This commit is contained in:
Paul Nothaft
2026-07-03 08:17:14 +02:00
parent b5eafc52bd
commit 2ea26a4962
25 changed files with 2272 additions and 2272 deletions
+1 -1
View File
@@ -1177,7 +1177,7 @@ async function runBackupInternal(isManual = false) {
async function startBackupService() {
try {
const config = await resolveConfigWithFallback();
const config = await resolveConfigWithFallback();
if (!config || !normalizeBoolean(config.backup_enabled)) {
if (backupJob) {
backupJob.stop();
+23 -23
View File
@@ -51,29 +51,29 @@ async function listContracts({ filters = {}, sort = 'issue_desc', page = 1, page
const total = ensureInt(totalRow?.total || 0);
switch (sort) {
case 'oldest':
query = query.orderBy('contracts.created_at', 'asc').orderBy('contracts.id', 'asc');
break;
case 'issue_asc':
query = query.orderBy('contracts.issue_date', 'asc').orderBy('contracts.id', 'asc');
break;
case 'issue_desc':
query = query.orderBy('contracts.issue_date', 'desc').orderBy('contracts.id', 'desc');
break;
case 'customer_asc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) asc')
.orderBy('contracts.id', 'desc');
break;
case 'customer_desc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) desc')
.orderBy('contracts.id', 'desc');
break;
case 'newest':
default:
query = query.orderBy('contracts.created_at', 'desc').orderBy('contracts.id', 'desc');
break;
case 'oldest':
query = query.orderBy('contracts.created_at', 'asc').orderBy('contracts.id', 'asc');
break;
case 'issue_asc':
query = query.orderBy('contracts.issue_date', 'asc').orderBy('contracts.id', 'asc');
break;
case 'issue_desc':
query = query.orderBy('contracts.issue_date', 'desc').orderBy('contracts.id', 'desc');
break;
case 'customer_asc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) asc')
.orderBy('contracts.id', 'desc');
break;
case 'customer_desc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) desc')
.orderBy('contracts.id', 'desc');
break;
case 'newest':
default:
query = query.orderBy('contracts.created_at', 'desc').orderBy('contracts.id', 'desc');
break;
}
const offset = Math.max(0, (page - 1) * pageSize);
@@ -78,8 +78,8 @@ async function buildPlaceholderContext(contract, customer) {
: '';
const customerAddress = customer
? [customer.address_line1, customer.address_line2, customer.postal_code, customer.city]
.filter(Boolean)
.join(', ')
.filter(Boolean)
.join(', ')
: '';
return {
+22 -22
View File
@@ -73,21 +73,21 @@ async function recordCustomerSignature({ token, name, ip, signatureDataUrl, acce
// consistent and saves a redundant read.
const persistedIp = await maybeStoreIp(ip);
try {
await db.transaction(async (trx) => {
await trx('contracts').where({ id: contract.id }).update({
status: 'signed_by_customer',
signed_by_customer_at: now,
signed_customer_name: String(name).trim(),
signed_customer_ip: persistedIp,
signed_customer_signature_path: signaturePath,
updated_at: now,
await db.transaction(async (trx) => {
await trx('contracts').where({ id: contract.id }).update({
status: 'signed_by_customer',
signed_by_customer_at: now,
signed_customer_name: String(name).trim(),
signed_customer_ip: persistedIp,
signed_customer_signature_path: signaturePath,
updated_at: now,
});
await trx('contract_action_tokens').where({ id: tokenRow.id }).update({
used_at: now,
used_action: 'signed_by_customer',
used_ip: persistedIp,
});
});
await trx('contract_action_tokens').where({ id: tokenRow.id }).update({
used_at: now,
used_action: 'signed_by_customer',
used_ip: persistedIp,
});
});
} catch (txErr) {
// C.7 — clean up the orphan signature PNG we wrote before the
// transaction. The DB rollback already undid the contract +
@@ -230,14 +230,14 @@ async function recordAdminCountersignature(contractId, { name, ip, signatureData
const newStatus = contract.status === 'signed_by_customer' ? 'fully_signed' : 'signed_by_admin';
const persistedAdminIp = await maybeStoreIp(ip);
try {
await db('contracts').where({ id: contract.id }).update({
status: newStatus,
signed_by_admin_at: now,
signed_admin_name: String(name).trim(),
signed_admin_ip: persistedAdminIp,
signed_admin_signature_path: signaturePath,
updated_at: now,
});
await db('contracts').where({ id: contract.id }).update({
status: newStatus,
signed_by_admin_at: now,
signed_admin_name: String(name).trim(),
signed_admin_ip: persistedAdminIp,
signed_admin_signature_path: signaturePath,
updated_at: now,
});
} catch (updateErr) {
// C.7 — clean up the orphan signature PNG if the contract row
// update threw. Best-effort; log on cleanup failure and re-throw
+5 -5
View File
@@ -329,11 +329,11 @@ async function createInvoice(payload, adminId, trx = db) {
* code should reach for the clearer `spawnInstallmentInvoices`.
*/
async function spawnInstallmentInvoices({ trx, eventId, quoteId, customer, currency, language,
lineItems, totals, installments, eventDate, adminId,
ccPdfEmail, netDays,
eventName, eventTimeStart, eventTimeEnd,
paymentNetDaysTemplateId, paymentTimingTemplateId,
paymentTermSnapshot, dealUuid, hold = false }) {
lineItems, totals, installments, eventDate, adminId,
ccPdfEmail, netDays,
eventName, eventTimeStart, eventTimeEnd,
paymentNetDaysTemplateId, paymentTimingTemplateId,
paymentTermSnapshot, dealUuid, hold = false }) {
// Monthly-billing intercept (migration 128). Quote → invoice
// conversion for a monthly-mode customer doesn't fan out N
// installment invoices — the customer pays one consolidated bill
+14 -14
View File
@@ -45,20 +45,20 @@ function computeScheduledSendAt(trigger, offsetDays, eventDate, baseDate = new D
const offset = ensureInt(offsetDays) * ms;
const eventTs = eventDate ? new Date(eventDate).getTime() : null;
switch (trigger) {
case 'quote_accepted':
return new Date(baseDate.getTime() + offset);
case 'before_event':
case 'after_event':
if (!eventTs) return new Date(baseDate.getTime() + offset);
return new Date(eventTs + offset);
case 'after_delivery':
// Treat as event_date + 14 days as a sensible default; admin can
// edit the scheduled_send_at on the invoice later.
if (!eventTs) return new Date(baseDate.getTime() + 14 * ms + offset);
return new Date(eventTs + 14 * ms + offset);
case 'fixed_date':
default:
return new Date(baseDate.getTime() + offset);
case 'quote_accepted':
return new Date(baseDate.getTime() + offset);
case 'before_event':
case 'after_event':
if (!eventTs) return new Date(baseDate.getTime() + offset);
return new Date(eventTs + offset);
case 'after_delivery':
// Treat as event_date + 14 days as a sensible default; admin can
// edit the scheduled_send_at on the invoice later.
if (!eventTs) return new Date(baseDate.getTime() + 14 * ms + offset);
return new Date(eventTs + 14 * ms + offset);
case 'fixed_date':
default:
return new Date(baseDate.getTime() + offset);
}
}
@@ -116,7 +116,7 @@ async function replaceReconciliationLine(
const subtotal = topLineSubtotal != null
? topLineSubtotal
: nonRecon.filter((x) => x.parent_position == null)
.reduce((s, x) => s + ensureInt(x.line_total_minor), 0);
.reduce((s, x) => s + ensureInt(x.line_total_minor), 0);
const adjustment = netSlice - subtotal;
if (adjustment === 0) return;
+29 -29
View File
@@ -67,35 +67,35 @@ async function listInvoices({ filters = {}, sort = 'issue_desc', page = 1, pageS
const total = ensureInt(countRow?.total || 0);
switch (sort) {
// "Newest" / "Oldest" means newest/oldest by CREATION time, not
// by issue_date. Issue_date is admin-controlled (used for tax
// accruals, retro-dating, future-dating) so it can drift from
// actual chronology — sorting by it makes a just-created invoice
// disappear into the middle of the list whenever its issue_date
// is set to something other than today. created_at always
// reflects when the row landed in the DB. id is the tiebreaker
// for rows that share a created_at second.
case 'oldest': query = query.orderBy('invoices.created_at', 'asc').orderBy('invoices.id', 'asc'); break;
case 'issue_asc': query = query.orderBy('invoices.issue_date', 'asc').orderBy('invoices.id', 'asc'); break;
case 'issue_desc': query = query.orderBy('invoices.issue_date', 'desc').orderBy('invoices.id', 'desc'); break;
case 'due_asc': query = query.orderBy('invoices.due_date', 'asc'); break;
case 'due_desc': query = query.orderBy('invoices.due_date', 'desc'); break;
case 'value_asc': query = query.orderBy('invoices.total_amount_minor', 'asc'); break;
case 'value_desc': query = query.orderBy('invoices.total_amount_minor', 'desc'); break;
case 'customer_asc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) asc')
.orderBy('invoices.id', 'desc');
break;
case 'customer_desc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) desc')
.orderBy('invoices.id', 'desc');
break;
case 'newest':
default:
query = query.orderBy('invoices.created_at', 'desc').orderBy('invoices.id', 'desc');
break;
// "Newest" / "Oldest" means newest/oldest by CREATION time, not
// by issue_date. Issue_date is admin-controlled (used for tax
// accruals, retro-dating, future-dating) so it can drift from
// actual chronology — sorting by it makes a just-created invoice
// disappear into the middle of the list whenever its issue_date
// is set to something other than today. created_at always
// reflects when the row landed in the DB. id is the tiebreaker
// for rows that share a created_at second.
case 'oldest': query = query.orderBy('invoices.created_at', 'asc').orderBy('invoices.id', 'asc'); break;
case 'issue_asc': query = query.orderBy('invoices.issue_date', 'asc').orderBy('invoices.id', 'asc'); break;
case 'issue_desc': query = query.orderBy('invoices.issue_date', 'desc').orderBy('invoices.id', 'desc'); break;
case 'due_asc': query = query.orderBy('invoices.due_date', 'asc'); break;
case 'due_desc': query = query.orderBy('invoices.due_date', 'desc'); break;
case 'value_asc': query = query.orderBy('invoices.total_amount_minor', 'asc'); break;
case 'value_desc': query = query.orderBy('invoices.total_amount_minor', 'desc'); break;
case 'customer_asc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) asc')
.orderBy('invoices.id', 'desc');
break;
case 'customer_desc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) desc')
.orderBy('invoices.id', 'desc');
break;
case 'newest':
default:
query = query.orderBy('invoices.created_at', 'desc').orderBy('invoices.id', 'desc');
break;
}
const offset = Math.max(0, (page - 1) * pageSize);
+1 -1
View File
@@ -358,7 +358,7 @@ async function sendStorno(stornoId, adminId) {
// email body — customers' bookkeepers expect to see the pair.
const originalRow = storno.cancels_invoice_id
? await db('invoices').where({ id: storno.cancels_invoice_id })
.select('invoice_number', 'issue_date').first()
.select('invoice_number', 'issue_date').first()
: null;
const { to: stornoTo, cc: stornoCc } = resolveBillingRecipients(customer, storno.cc_pdf_email);
+38 -38
View File
@@ -400,40 +400,40 @@ async function listQuotes({ filters = {}, sort = 'issue_desc', page = 1, pageSiz
const total = ensureInt(totalRow?.total || 0);
switch (sort) {
// "Newest" / "Oldest" sort by CREATION time, not issue_date —
// the latter is admin-controlled (retro-dated quotes, future-
// dated quotes for accruals) and drifts from actual chronology.
// Sorting by created_at always puts a just-saved quote at the
// top of the "Newest first" list.
case 'oldest':
query = query.orderBy('quotes.created_at', 'asc').orderBy('quotes.id', 'asc');
break;
case 'issue_asc':
query = query.orderBy('quotes.issue_date', 'asc').orderBy('quotes.id', 'asc');
break;
case 'issue_desc':
query = query.orderBy('quotes.issue_date', 'desc').orderBy('quotes.id', 'desc');
break;
case 'customer_asc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) asc')
.orderBy('quotes.id', 'desc');
break;
case 'customer_desc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) desc')
.orderBy('quotes.id', 'desc');
break;
case 'value_asc':
query = query.orderBy('quotes.total_amount_minor', 'asc');
break;
case 'value_desc':
query = query.orderBy('quotes.total_amount_minor', 'desc');
break;
case 'newest':
default:
query = query.orderBy('quotes.created_at', 'desc').orderBy('quotes.id', 'desc');
break;
// "Newest" / "Oldest" sort by CREATION time, not issue_date —
// the latter is admin-controlled (retro-dated quotes, future-
// dated quotes for accruals) and drifts from actual chronology.
// Sorting by created_at always puts a just-saved quote at the
// top of the "Newest first" list.
case 'oldest':
query = query.orderBy('quotes.created_at', 'asc').orderBy('quotes.id', 'asc');
break;
case 'issue_asc':
query = query.orderBy('quotes.issue_date', 'asc').orderBy('quotes.id', 'asc');
break;
case 'issue_desc':
query = query.orderBy('quotes.issue_date', 'desc').orderBy('quotes.id', 'desc');
break;
case 'customer_asc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) asc')
.orderBy('quotes.id', 'desc');
break;
case 'customer_desc':
query = query
.orderByRaw('COALESCE(customer_accounts.company_name, customer_accounts.last_name, customer_accounts.email) desc')
.orderBy('quotes.id', 'desc');
break;
case 'value_asc':
query = query.orderBy('quotes.total_amount_minor', 'asc');
break;
case 'value_desc':
query = query.orderBy('quotes.total_amount_minor', 'desc');
break;
case 'newest':
default:
query = query.orderBy('quotes.created_at', 'desc').orderBy('quotes.id', 'desc');
break;
}
const offset = Math.max(0, (page - 1) * pageSize);
@@ -1508,8 +1508,8 @@ async function convertToInvoiceOnly(quoteId, adminId, options = {}) {
const paymentTermSnapshot = quote.payment_term_snapshot
? (typeof quote.payment_term_snapshot === 'string'
? JSON.parse(quote.payment_term_snapshot)
: quote.payment_term_snapshot)
? JSON.parse(quote.payment_term_snapshot)
: quote.payment_term_snapshot)
: null;
const invoiceService = require('./invoiceService');
@@ -1621,8 +1621,8 @@ async function convertToEvent(quoteId, adminId, options = {}) {
const paymentTermSnapshot = quote.payment_term_snapshot
? (typeof quote.payment_term_snapshot === 'string'
? JSON.parse(quote.payment_term_snapshot)
: quote.payment_term_snapshot)
? JSON.parse(quote.payment_term_snapshot)
: quote.payment_term_snapshot)
: null;
// Lazy import to avoid the circular dep.
+22 -22
View File
@@ -200,20 +200,20 @@ class RestoreService {
// Step 6: Perform the actual restore based on type
let restoreResult;
switch (options.restoreType) {
case 'full':
restoreResult = await this.performFullRestore(localBackupPath, manifest, options);
break;
case 'database':
restoreResult = await this.performDatabaseRestore(localBackupPath, manifest, options);
break;
case 'files':
restoreResult = await this.performFilesRestore(localBackupPath, manifest, options);
break;
case 'selective':
restoreResult = await this.performSelectiveRestore(localBackupPath, manifest, options);
break;
default:
throw new Error(`Unknown restore type: ${options.restoreType}`);
case 'full':
restoreResult = await this.performFullRestore(localBackupPath, manifest, options);
break;
case 'database':
restoreResult = await this.performDatabaseRestore(localBackupPath, manifest, options);
break;
case 'files':
restoreResult = await this.performFilesRestore(localBackupPath, manifest, options);
break;
case 'selective':
restoreResult = await this.performSelectiveRestore(localBackupPath, manifest, options);
break;
default:
throw new Error(`Unknown restore type: ${options.restoreType}`);
}
// Step 7: Post-restore verification
@@ -308,7 +308,7 @@ class RestoreService {
this.log('info', 'Post-restore migrations applied');
} catch (migErr) {
this.log('warn',
`Post-restore migrate:safe failed — restore data is in place but the schema may lag the running image. ` +
'Post-restore migrate:safe failed — restore data is in place but the schema may lag the running image. ' +
`A container restart will retry via wait-for-db.sh. Error: ${migErr.message}`);
}
@@ -409,8 +409,8 @@ class RestoreService {
if (restoreRun) {
const failureMessage = rollbackAttempted
? (rollbackSucceeded
? `${error.message} (rolled back successfully to pre-restore state)`
: `${error.message} | ROLLBACK ALSO FAILED: ${rollbackError} — destination is in a partial state, inspect before retrying`)
? `${error.message} (rolled back successfully to pre-restore state)`
: `${error.message} | ROLLBACK ALSO FAILED: ${rollbackError} — destination is in a partial state, inspect before retrying`)
: `${error.message} (no pre-restore backup available — destination may be partial)`;
await db('restore_runs').where('id', restoreRun.id).update({
completed_at: new Date(),
@@ -888,9 +888,9 @@ class RestoreService {
throw new Error(
`Database backup file not found. Tried: ${candidates.join(', ')}. ` +
`Manifest recorded path: ${dbBackupFile}. ` +
`Hint: this usually means the manifest's database.backup_file path no longer ` +
`exists on disk (deleted? moved? volume not mounted?). Check ` +
`~/<your-compose-dir>/backup/database/ on the host.`
'Hint: this usually means the manifest\'s database.backup_file path no longer ' +
'exists on disk (deleted? moved? volume not mounted?). Check ' +
'~/<your-compose-dir>/backup/database/ on the host.'
);
}
@@ -1043,8 +1043,8 @@ class RestoreService {
await spawnAsync('psql', [
'-h', host, '-p', String(port), '-U', user, '-d', maintenanceDb,
'-c',
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity ` +
`WHERE datname = '${database.replace(/'/g, "''")}' AND pid <> pg_backend_pid()`,
'SELECT pg_terminate_backend(pid) FROM pg_stat_activity ' +
`WHERE datname = '${database.replace(/'/g, '\'\'')}' AND pid <> pg_backend_pid()`,
], { env });
// Drop and recreate database (extremely dangerous!)
+9 -9
View File
@@ -494,14 +494,14 @@ class S3StorageAdapter extends stream.EventEmitter {
let command;
switch (operation.toLowerCase()) {
case 'getobject':
command = new GetObjectCommand(params);
break;
case 'putobject':
command = new PutObjectCommand(params);
break;
default:
throw new Error(`Unsupported operation: ${operation}`);
case 'getobject':
command = new GetObjectCommand(params);
break;
case 'putobject':
command = new PutObjectCommand(params);
break;
default:
throw new Error(`Unsupported operation: ${operation}`);
}
return await getSignedUrl(this.s3Client, command, {
@@ -638,7 +638,7 @@ class S3StorageAdapter extends stream.EventEmitter {
UploadId: uploadId
}));
} catch (abortError) {
logger.error(`Failed to abort multipart upload:`, abortError);
logger.error('Failed to abort multipart upload:', abortError);
}
throw error;
@@ -64,8 +64,8 @@ class WatermarkGeneratorService {
const storageKey = resolvePhotoStorageKey(event, photo);
const result = storageKey
? await withLocalCopy(storageKey, (lp) =>
watermarkService.generateAndSaveWatermark(photo, lp, settings)
)
watermarkService.generateAndSaveWatermark(photo, lp, settings)
)
: await watermarkService.generateAndSaveWatermark(photo, resolvePhotoFilePath(event, photo), settings);
if (result.success) {
@@ -170,8 +170,8 @@ class WatermarkGeneratorService {
const storageKey = resolvePhotoStorageKey(event, photo);
const result = storageKey
? await withLocalCopy(storageKey, (lp) =>
watermarkService.generateAndSaveWatermark(photo, lp, settings)
)
watermarkService.generateAndSaveWatermark(photo, lp, settings)
)
: await watermarkService.generateAndSaveWatermark(photo, resolvePhotoFilePath(event, photo), settings);
if (result.success) {