Back to Index
ProKure Database

Rate Contracts Logic

Comprehensive documentation of the Rate Contract management system - storing pre-negotiated vendor pricing from awarded RFQs, enabling quick PO creation at agreed rates without repeated quotations.

2 Tables (rfq.rate_contracts, rfq.rate_contract_terms)
Schema v1.0
RFQ Module
Updated: Feb 2026

What is a Rate Contract?

A Rate Contract is a pre-negotiated pricing agreement established after an RFQ award. Instead of running a full RFQ cycle for each purchase, the buyer can directly create POs referencing the rate contract, ensuring consistent pricing, faster procurement, and reduced administrative overhead.

Benefits

  • Faster PO creation (skip RFQ cycle)
  • Locked-in pricing for budget predictability
  • Pre-approved vendor and terms
  • Volume discounts and bulk pricing
  • Compliance with approved supplier lists

Common Use Cases

  • MRO supplies (bolts, fasteners, tools)
  • Stationery and office supplies
  • Standard components (sensors, valves)
  • Maintenance services (HVAC, electrical)
  • IT hardware and software licenses

Rate Contract Lifecycle

RFQ Published
Vendors Submit Bids
RFQ Awarded
Create Rate Contract from Awarded Line Items
Rate Contract Pending Approval
Approved
Active (within validity period)
PO Created
PO Type = CONTRACT_RELEASE
Pricing Auto-Populated
Utilization Tracked
Expired / Exhausted

Database Tables

rfq.rate_contracts

  • id - UUID primary key
  • rate_contract_number - System-generated unique identifier (e.g., RC-2026-001)
  • vendor_id - FK to vendors (awarded supplier)
  • item_id - FK to inventory.items (product/material)
  • item_description - Textual description (in case item master changes)
  • rfq_id - FK to rfqs (source RFQ from which this was awarded)
  • quotation_id - FK to quotations (winning quotation)
  • company_id - FK to companies
  • supply_rate - Base material/product price per unit
  • installation_rate - Installation/setup cost per unit (if applicable)
  • total_rate - Total rate = supply + installation
  • uom_id - FK to units_of_measure
  • tax_code_id - FK to tax_codes
  • tax_percent - Applicable tax percentage
  • currency_code - Contract currency (USD, INR, etc.)
  • valid_from - Contract start date
  • valid_until - Contract expiry date
  • min_order_quantity - Minimum order qty per release
  • max_order_quantity - Maximum order qty per release
  • total_quantity_allowed - Total cumulative quantity allowed (null = unlimited)
  • total_quantity_consumed - Running total of quantity ordered via POs
  • total_value_allowed - Total cumulative value allowed (null = unlimited)
  • total_value_consumed - Running total of PO value against this contract
  • delivery_days - Agreed lead time in days
  • status - DRAFT, PENDING_APPROVAL, APPROVED, ACTIVE, EXPIRED, EXHAUSTED, CANCELLED, REJECTED
  • approval_status - PENDING, APPROVED, REJECTED
  • approved_by - FK to users (approver)
  • approved_at - Timestamp of approval
  • rejected_by - FK to users (rejector)
  • rejected_at - Timestamp of rejection
  • rejection_reason - Why it was rejected
  • notes - Internal remarks
  • created_by, updated_by - Audit fields
  • created_at, updated_at - Timestamps
  • metadata - JSONB for extensibility

rfq.rate_contract_terms

  • id - UUID primary key
  • rate_contract_id - FK to rate_contracts (CASCADE on delete)
  • term_type - PAYMENT, DELIVERY, WARRANTY, PENALTY, GENERAL
  • term_category - Grouping (e.g., PAYMENT_SCHEDULE, LATE_DELIVERY_PENALTY)
  • term_text - Full text of the term/condition
  • is_mandatory - Whether this term must be accepted by buyer for each PO release
  • display_order - Sequence for UI display
  • created_at - Timestamp
  • metadata - JSONB

Creating a Rate Contract

1

RFQ Award Completion

After an RFQ is awarded (status = AWARDED), the procurement manager identifies line items suitable for rate contract establishment. Typically MRO items, standard components, or recurring services.

-- Identify awarded RFQ items for rate contract creation
SELECT
    q.id AS quotation_id,
    qi.id AS quotation_item_id,
    q.vendor_id,
    qi.item_id,
    qi.item_description,
    qi.unit_price,
    qi.uom_id,
    qi.tax_code_id,
    r.id AS rfq_id,
    r.company_id
FROM rfq.quotations q
JOIN rfq.quotation_items qi ON qi.quotation_id = q.id
JOIN rfq.rfqs r ON r.id = q.rfq_id
WHERE q.is_selected = true
  AND r.status = 'AWARDED'
  AND r.id = @rfqId;
2

Generate Rate Contract Record

For each selected quotation line item, create a rate contract record. The system auto-generates the rate_contract_number and sets initial status to DRAFT or PENDING_APPROVAL based on configuration.

-- Insert rate contract from awarded quotation item
INSERT INTO rfq.rate_contracts (
    rate_contract_number,
    vendor_id,
    item_id,
    item_description,
    rfq_id,
    quotation_id,
    company_id,
    supply_rate,
    installation_rate,
    total_rate,
    uom_id,
    tax_code_id,
    tax_percent,
    currency_code,
    valid_from,
    valid_until,
    min_order_quantity,
    max_order_quantity,
    delivery_days,
    status,
    approval_status,
    created_by,
    metadata
) VALUES (
    generate_rate_contract_number(@companyId),  -- e.g., RC-2026-001
    @vendorId,
    @itemId,
    @itemDescription,
    @rfqId,
    @quotationId,
    @companyId,
    @supplyRate,
    @installationRate,
    @supplyRate + @installationRate,
    @uomId,
    @taxCodeId,
    @taxPercent,
    @currencyCode,
    CURRENT_DATE,
    CURRENT_DATE + INTERVAL '1 year',  -- Default 1 year validity
    @minQty,
    @maxQty,
    @deliveryDays,
    'PENDING_APPROVAL',
    'PENDING',
    @userId,
    '{}'::jsonb
);
3

Add Terms & Conditions

Attach specific terms copied from the awarded quotation or contract master. Payment terms, delivery terms, warranty clauses, penalty clauses, etc.

-- Insert rate contract terms
INSERT INTO rfq.rate_contract_terms (
    rate_contract_id,
    term_type,
    term_category,
    term_text,
    is_mandatory,
    display_order
) VALUES
(@rateContractId, 'PAYMENT', 'PAYMENT_TERMS', 'Net 30 days from invoice date', true, 1),
(@rateContractId, 'DELIVERY', 'LEAD_TIME', '15 working days from PO date', true, 2),
(@rateContractId, 'WARRANTY', 'WARRANTY_PERIOD', '12 months warranty from delivery', false, 3),
(@rateContractId, 'PENALTY', 'LATE_DELIVERY', '0.5% penalty per week for delayed delivery', true, 4);
4

Approval Workflow

If approval is required (based on rate contract value or company policy), route through the approval matrix. Approver verifies pricing, validity period, T&C, and vendor reliability.

Approval Logic

Uses the standard approval.approval_matrix with document_type='RATE_CONTRACT'. Multi-level approvals may be required for high-value contracts or long validity periods.

-- Approve rate contract
UPDATE rfq.rate_contracts
SET
    approval_status = 'APPROVED',
    status = 'ACTIVE',
    approved_by = @approverId,
    approved_at = NOW(),
    updated_at = NOW()
WHERE id = @rateContractId;

Using Rate Contracts in PO Creation

PO Type: CONTRACT_RELEASE

When creating a PO against a rate contract, set po_type = 'CONTRACT_RELEASE' and reference the rate_contract_id. The system auto-populates pricing, vendor, terms, and tax details.

1

Search Active Rate Contracts

Buyer searches for active rate contracts by item, vendor, or rate contract number. The system shows only contracts within validity period and not yet exhausted.

-- Search active rate contracts for a given item
SELECT
    rc.id,
    rc.rate_contract_number,
    v.vendor_name,
    i.item_code,
    i.item_name,
    rc.total_rate,
    u.uom_code,
    rc.valid_from,
    rc.valid_until,
    rc.delivery_days,
    rc.total_quantity_allowed,
    rc.total_quantity_consumed,
    (rc.total_quantity_allowed - rc.total_quantity_consumed) AS available_quantity
FROM rfq.rate_contracts rc
JOIN vendor.vendors v ON v.id = rc.vendor_id
JOIN inventory.items i ON i.id = rc.item_id
JOIN master.units_of_measure u ON u.id = rc.uom_id
WHERE rc.company_id = @companyId
  AND rc.status = 'ACTIVE'
  AND rc.valid_from <= CURRENT_DATE
  AND rc.valid_until >= CURRENT_DATE
  AND (rc.total_quantity_allowed IS NULL OR rc.total_quantity_consumed < rc.total_quantity_allowed)
  AND i.id = @itemId
ORDER BY rc.total_rate ASC;
2

Create PO with Rate Contract Reference

Create the PO header with po_type='CONTRACT_RELEASE' and link to the rate contract. Pricing and terms auto-populate from the contract.

-- Create PO against rate contract
INSERT INTO procurement.purchase_orders (
    po_number,
    company_id,
    vendor_id,
    po_type,
    rate_contract_id,
    buyer_id,
    po_date,
    status,
    currency_code,
    payment_terms_id,
    delivery_terms_id,
    created_by
) VALUES (
    generate_po_number(@companyId),
    @companyId,
    @vendorId,  -- From rate contract
    'CONTRACT_RELEASE',
    @rateContractId,
    @buyerId,
    CURRENT_DATE,
    'DRAFT',
    @currencyCode,  -- From rate contract
    @paymentTermsId,  -- From rate contract terms
    @deliveryTermsId,  -- From rate contract terms
    @userId
);
3

Add PO Line Items with Contract Pricing

PO line items inherit unit price, tax code, and UOM from the rate contract. Quantity can vary per release within contract limits.

-- Add PO line item referencing rate contract
INSERT INTO procurement.purchase_order_items (
    po_id,
    line_number,
    item_id,
    item_description,
    quantity,
    uom_id,
    unit_price,
    tax_code_id,
    tax_amount,
    line_total,
    rate_contract_id,
    delivery_date
) VALUES (
    @poId,
    1,
    @itemId,  -- From rate contract
    @itemDescription,
    @quantity,  -- User input (within min/max limits)
    @uomId,  -- From rate contract
    @totalRate,  -- From rate contract (supply + installation)
    @taxCodeId,  -- From rate contract
    @quantity * @totalRate * (@taxPercent / 100),
    @quantity * @totalRate * (1 + @taxPercent / 100),
    @rateContractId,
    CURRENT_DATE + (@deliveryDays || ' days')::interval  -- From rate contract
);
4

Update Rate Contract Utilization

After PO approval, update the rate contract's consumed quantity and value. This prevents over-utilization and triggers expiry when limits are reached.

-- Update rate contract utilization after PO approval
UPDATE rfq.rate_contracts
SET
    total_quantity_consumed = total_quantity_consumed + @poQuantity,
    total_value_consumed = total_value_consumed + @poLineTotal,
    status = CASE
        WHEN total_quantity_allowed IS NOT NULL
             AND (total_quantity_consumed + @poQuantity) >= total_quantity_allowed
        THEN 'EXHAUSTED'
        ELSE status
    END,
    updated_at = NOW()
WHERE id = @rateContractId;

Rate Contract Status Management

Status Description Trigger
DRAFT Initial state, not yet submitted for approval Created by buyer
PENDING_APPROVAL Submitted for approval workflow Buyer submits
APPROVED Approved but not yet active (before valid_from date) All approvers approve
ACTIVE Currently valid and usable for PO creation valid_from ≤ today ≤ valid_until, within limits
EXPIRED Validity period ended System job checks valid_until
EXHAUSTED Quantity or value limit reached total_quantity_consumed ≥ total_quantity_allowed
CANCELLED Manually cancelled by buyer/admin User action
REJECTED Rejected during approval workflow Approver rejects

Automated Status Updates

A scheduled system job (daily at midnight) checks all ACTIVE rate contracts and updates their status to EXPIRED if valid_until < CURRENT_DATE. Similarly, contracts are marked EXHAUSTED when utilization limits are reached during PO approval.

Reporting & Analytics

1

Rate Contract Utilization Report

Track how much of each rate contract has been consumed, identify underutilized contracts, and forecast expiry dates.

-- Rate contract utilization summary
SELECT
    rc.rate_contract_number,
    v.vendor_name,
    i.item_name,
    rc.total_quantity_allowed,
    rc.total_quantity_consumed,
    ROUND((rc.total_quantity_consumed::numeric / NULLIF(rc.total_quantity_allowed, 0)) * 100, 2) AS utilization_percent,
    rc.total_value_allowed,
    rc.total_value_consumed,
    rc.valid_from,
    rc.valid_until,
    (rc.valid_until - CURRENT_DATE) AS days_remaining,
    rc.status
FROM rfq.rate_contracts rc
JOIN vendor.vendors v ON v.id = rc.vendor_id
JOIN inventory.items i ON i.id = rc.item_id
WHERE rc.company_id = @companyId
  AND rc.status IN ('ACTIVE', 'APPROVED')
ORDER BY utilization_percent DESC;
2

Cost Savings Analysis

Compare rate contract pricing vs. current market rates to measure cost savings achieved through locked-in pricing.

-- Cost savings via rate contracts (vs. spot pricing)
SELECT
    rc.rate_contract_number,
    i.item_name,
    rc.total_rate AS contract_rate,
    i.standard_cost AS current_market_rate,
    (i.standard_cost - rc.total_rate) AS savings_per_unit,
    rc.total_quantity_consumed,
    ((i.standard_cost - rc.total_rate) * rc.total_quantity_consumed) AS total_savings
FROM rfq.rate_contracts rc
JOIN inventory.items i ON i.id = rc.item_id
WHERE rc.company_id = @companyId
  AND rc.status = 'ACTIVE'
  AND i.standard_cost > rc.total_rate
ORDER BY total_savings DESC;
3

Vendor Performance by Rate Contract

Evaluate vendor reliability in fulfilling rate contract commitments (on-time delivery, quality, compliance).

-- Vendor performance against rate contracts
SELECT
    v.vendor_name,
    COUNT(DISTINCT rc.id) AS total_contracts,
    COUNT(DISTINCT po.id) AS total_pos_released,
    AVG(vs.on_time_delivery_pct) AS avg_otd,
    AVG(vs.quality_score) AS avg_quality,
    SUM(rc.total_value_consumed) AS total_business_value
FROM rfq.rate_contracts rc
JOIN vendor.vendors v ON v.id = rc.vendor_id
LEFT JOIN procurement.purchase_orders po ON po.rate_contract_id = rc.id
LEFT JOIN vendor.vendor_scores vs ON vs.vendor_id = v.id AND vs.is_latest = true
WHERE rc.company_id = @companyId
GROUP BY v.id, v.vendor_name
ORDER BY total_business_value DESC;

Business Rules & Validations

Rule ID Description Enforcement Point
BR-RC-001 Rate contract can only be created from an AWARDED RFQ quotation Rate contract creation
BR-RC-002 valid_from must be ≤ valid_until Rate contract creation/update
BR-RC-003 total_quantity_consumed cannot exceed total_quantity_allowed PO approval (prevents over-commitment)
BR-RC-004 PO quantity must be within min_order_quantity and max_order_quantity PO line item creation
BR-RC-005 Only ACTIVE rate contracts can be referenced in new POs PO creation
BR-RC-006 Rate contract cannot be deleted if referenced by any PO Rate contract deletion (prevents orphaned POs)
BR-RC-007 Approval required if total_value_allowed exceeds company threshold Rate contract creation
BR-RC-008 Vendor must be APPROVED status to be included in rate contract Rate contract creation
BR-RC-009 Cannot modify pricing or vendor after approval (requires cancellation + new contract) Rate contract update
BR-RC-010 System auto-expires contracts daily where valid_until < CURRENT_DATE Scheduled job (daily)

.NET API Implementation

public class RateContractService
{
    // Create rate contract from awarded RFQ quotation
    public async Task<RateContractDto> CreateFromQuotationAsync(
        Guid quotationId,
        Guid quotationItemId,
        RateContractCreateDto dto,
        Guid userId)
    {
        // 1. Validate quotation is from awarded RFQ
        var quotation = await _quotationRepo.GetByIdAsync(quotationId);
        if (!quotation.IsSelected || quotation.Rfq.Status != "AWARDED")
            throw new BusinessException("BR-RC-001");

        // 2. Create rate contract
        var rateContract = new RateContract
        {
            RateContractNumber = await GenerateRateContractNumberAsync(dto.CompanyId),
            VendorId = quotation.VendorId,
            ItemId = quotationItem.ItemId,
            ItemDescription = quotationItem.ItemDescription,
            RfqId = quotation.RfqId,
            QuotationId = quotationId,
            CompanyId = dto.CompanyId,
            SupplyRate = quotationItem.UnitPrice,
            InstallationRate = dto.InstallationRate ?? 0,
            TotalRate = quotationItem.UnitPrice + (dto.InstallationRate ?? 0),
            UomId = quotationItem.UomId,
            TaxCodeId = quotationItem.TaxCodeId,
            TaxPercent = quotationItem.TaxAmount / quotationItem.UnitPrice * 100,
            CurrencyCode = quotation.CurrencyCode,
            ValidFrom = dto.ValidFrom ?? DateTime.Today,
            ValidUntil = dto.ValidUntil ?? DateTime.Today.AddYears(1),
            Status = "PENDING_APPROVAL",
            ApprovalStatus = "PENDING",
            CreatedBy = userId
        };

        await _rateContractRepo.AddAsync(rateContract);

        // 3. Submit for approval if required
        await _approvalService.SubmitForApprovalAsync(
            "RATE_CONTRACT",
            rateContract.Id,
            rateContract.TotalValueAllowed ?? 0,
            userId);

        return MapToDto(rateContract);
    }

    // Search active rate contracts for PO creation
    public async Task<List<RateContractDto>> SearchActiveContractsAsync(
        Guid companyId,
        Guid? itemId = null,
        Guid? vendorId = null)
    {
        return await _rateContractRepo.GetActiveContractsAsync(
            companyId, itemId, vendorId);
    }

    // Update utilization after PO approval
    public async Task UpdateUtilizationAsync(
        Guid rateContractId,
        decimal quantity,
        decimal value)
    {
        var contract = await _rateContractRepo.GetByIdAsync(rateContractId);

        contract.TotalQuantityConsumed += quantity;
        contract.TotalValueConsumed += value;

        // Check if exhausted
        if (contract.TotalQuantityAllowed.HasValue &&
            contract.TotalQuantityConsumed >= contract.TotalQuantityAllowed)
        {
            contract.Status = "EXHAUSTED";
        }

        await _rateContractRepo.UpdateAsync(contract);
    }

    // Daily job: Expire contracts past validity
    public async Task ExpireContractsAsync()
    {
        await _rateContractRepo.BulkUpdateStatusAsync(
            status: "EXPIRED",
            whereClause: rc => rc.Status == "ACTIVE" &&
                               rc.ValidUntil < DateTime.Today);
    }
}