feat(quotes): admin decline-on-behalf with optional reason
Add a "Decline on behalf" action mirroring accept-on-behalf, for when a customer says no by phone/email. Flips a draft/sent/expired quote to declined, stamps declined_at, closes the public response window, and invalidates outstanding accept/decline tokens so the emailed link can't toggle it back. Optional free-text reason persisted to a new quotes.decline_reason column (migration 115) and shown on the quote detail page. Hard-delete intentionally not included.
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Migration: admin decline-quote reason.
|
||||
*
|
||||
* Background: admins can now decline a quote on the customer's behalf
|
||||
* ("customer told us by phone they're not going ahead") instead of
|
||||
* waiting for the public response link. This stores an optional free-text
|
||||
* reason alongside the existing `declined_at` timestamp so the quote
|
||||
* detail page can show WHY it was declined.
|
||||
*
|
||||
* Nullable, no default — existing declined rows simply carry no reason,
|
||||
* which is exactly how customer-side declines already look. No behaviour
|
||||
* change on upgrade.
|
||||
*
|
||||
* Idempotent: guarded by hasColumn so a re-run is a no-op.
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
if (!(await knex.schema.hasTable('quotes'))) return;
|
||||
if (await knex.schema.hasColumn('quotes', 'decline_reason')) return;
|
||||
await knex.schema.alterTable('quotes', (table) => {
|
||||
table.text('decline_reason');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
if (!(await knex.schema.hasTable('quotes'))) return;
|
||||
if (!(await knex.schema.hasColumn('quotes', 'decline_reason'))) return;
|
||||
await knex.schema.alterTable('quotes', (table) => {
|
||||
table.dropColumn('decline_reason');
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user