Skip to main content

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 when validUntil passes (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 updateMany queries 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' },
});
}