Quotation Lifecycle & Expiry
Every cost estimation generates a Quotation record that transitions through a structured lifecycle. This document details the quotation statuses, quote numbering system, and automatic expiry logic.
🔄 Lifecycle State Machine
A quotation moves through the following states:
📋 Status Transitions
DRAFT: Built for future campaign planner features (not used in standard estimate requests).ISSUED: Quotation is active and locked against an immutable snapshot.EXPIRED: Set automatically whenvalidUntilpasses (default: 7 days validity).SUPERSEDED: Applied when a vendor recalculates estimations for the same target, rendering the older quote obsolete.
🏷️ Quote Numbering Convention
Quotations use a sequential, branded prefix formatting:
KQ - YYYY - NNNNN
KQ: Kloyst Quotation identifier.YYYY: The calendar year of creation (e.g.2026).NNNNN: A zero-padded sequential serial index (e.g.00042).
Generation Logic
const year = new Date().getFullYear();
const prefix = `KQ-${year}-`;
// Query the latest database quotation with the same prefix
const latest = await this.prisma.quotation.findFirst({
where: { quoteNumber: { startsWith: prefix } },
orderBy: { createdAt: 'desc' },
select: { quoteNumber: true },
});
let sequence = 1;
if (latest) {
const lastNum = parseInt(latest.quoteNumber.split('-').pop() || '0', 10);
sequence = lastNum + 1;
}
const quoteNumber = `${prefix}${String(sequence).padStart(5, '0')}`;
⏰ Automatic Expiry Cron Job
Quotations expire automatically after 7 days to protect against outdated Meta rates.
The Expiry Strategy
- A scheduled task runs daily at 3 AM (low-traffic hour).
- To avoid locking tables and impacting active users, the cron uses bulk
updateManyqueries instead of processing records individually in loops:
@Cron(CronExpression.EVERY_DAY_AT_3AM)
async expireStaleQuotations() {
await this.prisma.quotation.updateMany({
where: {
status: 'ISSUED',
validUntil: { lt: new Date() },
},
data: { status: 'EXPIRED' },
});
}