fix(crm): quote→event fallback resolves an ACTIVE event type, never hardcoded 'wedding'

The last-resort fallback hardcoded 'wedding', which breaks when the admin has
disabled that type. resolveDefaultEventType now prefers the generic 'other'
catch-all when active, else the first active type by display order, and only
uses a literal as a final guard if the catalog is empty/unreadable. The chosen
quote type and the crm_default_event_type setting still take precedence.
This commit is contained in:
Luca
2026-06-23 18:02:11 +02:00
parent f78671fc6c
commit 23098127a8
+22 -3
View File
@@ -305,6 +305,25 @@ async function nextQuoteNumber(trx) {
return formatNumberInTemplate(format, year, seq); return formatNumberInTemplate(format, year, seq);
} }
/**
* Resolve the fallback event type for a quote→event conversion when the quote
* itself carries none. Never hardcodes a specific slug (any of them, incl.
* 'other', can be disabled by the admin): prefer the generic 'other' catch-all
* when it's active, else the first active type by display order, and only fall
* back to the literal 'other' if the catalog is somehow empty/unreadable.
*/
async function resolveDefaultEventType(conn) {
const q = conn || db;
try {
const other = await q('event_types').where({ slug_prefix: 'other', is_active: true }).first('slug_prefix');
if (other) return 'other';
const firstActive = await q('event_types').where({ is_active: true }).orderBy('display_order', 'asc').first('slug_prefix');
return firstActive?.slug_prefix || 'other';
} catch (_) {
return 'other';
}
}
function ensureCustomerFeatureEnabled(customer, feature) { function ensureCustomerFeatureEnabled(customer, feature) {
// Global toggle (`customer_feature_quotes_enabled` / `..._bills_enabled`) // Global toggle (`customer_feature_quotes_enabled` / `..._bills_enabled`)
// is checked at the route layer (feature flag); here we only enforce // is checked at the route layer (feature flag); here we only enforce
@@ -1492,11 +1511,11 @@ async function convertToEvent(quoteId, adminId, options = {}) {
const adminEmail = adminRow?.email || customer.email || '[email protected]'; const adminEmail = adminRow?.email || customer.email || '[email protected]';
// Event type for the new event: the type chosen on the quote (migration 146), // Event type for the new event: the type chosen on the quote (migration 146),
// else a configurable org default, else 'wedding' as the last-resort seeded // else a configurable org default, else the resolved catch-all (an ACTIVE
// type. Replaces the old unconditional hardcoded 'wedding'. // type — never a hardcoded slug the admin may have disabled).
const eventType = (quote.event_type && String(quote.event_type).trim()) const eventType = (quote.event_type && String(quote.event_type).trim())
|| (await getAppSetting('crm_default_event_type')) || (await getAppSetting('crm_default_event_type'))
|| 'wedding'; || (await resolveDefaultEventType(trx));
// Each candidate column is paired with the value we'd write. We // Each candidate column is paired with the value we'd write. We
// ask the DB which columns exist and only keep the matching pairs // ask the DB which columns exist and only keep the matching pairs