Skip to content

Canvass Business Rules

This document outlines the business rules for the canvassing process in the PRS system.

Canvass Entity

A canvass is the process of collecting and comparing supplier quotations for items in a requisition.

Canvass Status Flow

Text Only
1
2
3
DRAFT → FOR_CS_APPROVAL → CS_APPROVED;
CS_REJECTED;
CS_CANCELLED
  • A canvass can be REJECTED at the approval stage.

Canvass Fields

Field Description Validation Rules
csNumber Canvass Sheet Number Auto-generated
csLetter Canvass Sheet Letter Auto-generated
requisitionId Associated Requisition ID Required
status Current status Must be one of the defined statuses
createdBy User ID of creator Required
assignedTo User ID of assignee Required
notes Additional notes Optional, max 100 characters

Business Rules

1. Canvass Creation

  • Only the user assigned to the requisition can create a canvass
  • User can create multiple canvass sheet
  • A canvass can be created as a draft or submitted immediately
  • Canvass Sheet Number (CS Number) is auto-generated
  • Canvass Sheet Letter (CS Letter) is auto-generated
  • Creator is set to the current assigned purchasing staff
  • Status is set to DRAFT based on isDraft flag and items included
  • RS Status will be set to RS_IN_PROGRESS once canvass is submitted
JavaScript
// Example code for CS Number generation
async function generateCSNumberCode(isDraft, canvassId) {
  const csLetter = 'A'; // Default letter

  if (isDraft) {
    return { csNumber: null, csLetter };
  }

  if (canvassId) {
    const existingCanvass = await canvassRepository.findOne({
      where: { id: canvassId },
    });

    if (existingCanvass.csNumber) {
      return { csNumber: existingCanvass.csNumber, csLetter: existingCanvass.csLetter };
    }
  }

  const latestCS = await canvassRepository.findOne({
    where: {
      csNumber: { [Sequelize.Op.not]: null },
    },
    order: [['csNumber', 'DESC']],
  });

  let csNumber = '00001';

  if (latestCS && latestCS.csNumber) {
    const latestNumber = parseInt(latestCS.csNumber, 10);
    csNumber = (latestNumber + 1).toString().padStart(5, '0');
  }

  return { csNumber, csLetter };
}

2. Canvass Validation

  • Requisition must exist and be in ASSIGNED status
  • Assigned user will be the creator of the canvass
  • Items must be valid and belong to the requisition
  • Item requested qty must not be fully canvassed
  • Suppliers must be provided for each item with price information
JavaScript
// Example validation logic
async function canvassCreateValidation(payload) {
  const { canvassId, requisitionId, creatorId, isDraft = true } = payload;
  const { CANVASS_STATUS } = constants.canvass;

  const existingRequisition = await requisitionRepository.findOne({
    where: { id: requisitionId },
  });

  if (!existingRequisition) {
    throw new Error('Requisition not found. Cannot create canvass without a valid requisition');
  }

  if (!existingRequisition.assignedTo) {
    throw new Error('Cannot create canvass. No assigned purchasing staff.');
  }

  if (existingRequisition.assignedTo !== creatorId) {
    throw new Error('Cannot create canvass. Requisition is assigned to another user');
  }

  return existingRequisition;
}

3. Canvass Items

  • Items can be added from the requisition as long as the requested quantity is not yet fully canvassed
  • For OFM, canvass quantity should not exceed requested quantity
  • For Non-OFM, allowed but will highlight exceeded quantity
  • Each item must have at least one supplier - up to 4 suppliers
  • Each supplier must have a price
  • Items can be partially canvassed (not all requisition items included)
  • Steelbars are shown in a separate tab

4. Canvass Suppliers

For Non-TOM Request:

  • Each canvass item can have 1 up to 4 suppliers
  • Suppliers must have valid information (name, contact, etc.)
  • Suppliers must provide a price for the item
  • Suppliers can offer discounts (percentage or fixed amount)

For TOM Request: - Each canvass item can have 1 supplier only, cannot add more supplier - Supplier drop-down would only contain list of COMPANIES and PROJECTS - Suppliers must provide a price for the item - for confirmation - Suppliers can offer discounts (percentage or fixed amount)

JavaScript
// Example supplier processing logic
async function processSuppliers({
  isDraft,
  suppliers,
  assignedTo,
  transaction,
  canvassItemId,
  existingSuppliers,
  isUpdateItems = false,
  userFromToken,
  canvassId,
}) {
  if (isUpdateItems) {
    const newSupplierIds = suppliers
      .filter((supplier) => supplier.id)
      .map((supplier) => supplier.id);

    const suppliersToDelete = existingSuppliers
      .filter((supplier) => !newSupplierIds.includes(supplier.id))
      .map((supplier) => supplier.id);

    if (suppliersToDelete.length > 0) {
      await canvassItemSupplierRepository.destroy(
        {
          id: {
            [Sequelize.Op.in]: suppliersToDelete,
          },
        },
        { transaction },
      );
    }
  }

  for (const supplier of suppliers) {
    const { id: supplierId, attachments = [], ...supplierData } = supplier;

    let canvassSupplier;
    if (supplierId) {
      canvassSupplier = await canvassItemSupplierRepository.update(
        { id: supplierId },
        {
          ...supplierData,
          assignedTo,
        },
        { transaction, returning: true },
      );
      canvassSupplier = canvassSupplier[1][0];
    } else {
      canvassSupplier = await canvassItemSupplierRepository.create(
        {
          ...supplierData,
          canvassItemId,
          assignedTo,
        },
        { transaction },
      );
    }

    // Process attachments
    // ...
  }
}

5. Canvass Approval

  • Only FOR_CS_APPROVAL canvasses can be approved
  • User must be an assigned approver for the canvass
  • Approvers are processed in order by level
  • If all approvers approve, status changes to CS_APPROVED
  • Status will remain as FOR_CS_APPROVAL until fully approved
  • Additional approvers can be added during the approval process
  • Approvers can input a note during approval
  • Approvers can edit item quantity or delete an item from the table
  • During Purchasing Head approval, atleast 1 (one) or more supplier per item should be chosen
  • Final Management approvals has 2 fixed approvers that can approve interchangeably
  • Upon final approval, Purchase Orders are automatically created, grouped by supplier and terms
JavaScript
// Example approval logic
async function approveCanvass({
  existingCanvass,
  approver,
  canvassSuppliers,
  transaction,
}) {
  const { USER_TYPES } = constants.user;
  const { CANVASS_STATUS, CANVASS_APPROVER_STATUS, CANVASS_ITEM_STATUS } = constants.canvass;

  const forApprovalStatuses = [
    CANVASS_STATUS.FOR_APPROVAL,
    CANVASS_STATUS.REJECTED,
  ];
  const isReadyForApproval = !forApprovalStatuses.includes(
    existingCanvass.status,
  );

  if (isReadyForApproval) {
    throw new Error('Canvass sheet is not for approval');
  }

  const approvers = await canvassApproverRepository.findAll({
    where: { canvassRequisitionId: existingCanvass.id },
    paginate: false,
    order: [
      ['level', 'ASC'],
      ['isAdhoc', 'ASC'],
    ],
  });

  // Process approvers
  // ...

  // Process selected suppliers
  if (canvassSuppliers.length > 0) {
    for (const supplier of canvassSuppliers) {
      await canvassItemSupplierRepository.update(
        { id: supplier.id },
        { isSelected: supplier.isSelected },
        { transaction },
      );
    }
  }

  // Check if all approvers have approved
  const pendingApprovers = approvers.filter(
    (a) => a.status === CANVASS_APPROVER_STATUS.PENDING,
  );

  if (pendingApprovers.length === 0) {
    await canvassRequisitionRepository.update(
      { id: existingCanvass.id },
      { status: CANVASS_STATUS.APPROVED },
      { transaction },
    );

    await canvassItemRepository.update(
      { canvassRequisitionId: existingCanvass.id },
      { status: CANVASS_ITEM_STATUS.APPROVED },
      { transaction },
    );

    return true; // All approved
  }

  return false; // Not all approved yet
}

6. Canvass Rejection

  • Only FOR_CS_APPROVAL canvasses can be rejected
  • User must be an assigned approver for the canvass
  • Rejection requires a reason/comment
  • Upon rejection, status changes to CS_REJECTED
  • Assigned Purchasing staff should be able to edit REJECTED canvass sheet
  • Can resubmit and will undergo approval process again with status FOR_CS_APPROVAL

Validation Rules

Detailed validation rules are defined in the Zod schema:

JavaScript
const createCanvassSchema = z
  .object({
    id: createNumberSchema('Canvass ID').optional(),
    isDraft: z.boolean({
      invalid_type_error: 'Invalid draft type',
      required_error: 'Draft type is required',
    }),
    requisitionId: createNumberSchema('Requisition ID'),
    addItems: z
      .array(addItemsSchema, {
        invalid_type_error: 'Invalid Canvass Items',
      })
      .optional(),
    updateItems: z
      .array(updateItemsSchema, {
        invalid_type_error: 'Invalid Canvass Items',
      })
      .optional(),
    deleteItems: z
      .array(deleteItemsSchema, {
        invalid_type_error: 'Invalid Canvass Item ID',
      })
      .optional(),
    notes: z
      .string(stringFieldError('Canvass note'))
      .trim()
      .max(100, 'Notes must not exceed 100 characters')
      .regex(
        /^[A-Za-z0-9'\s.-Ññ]+$/,
        'Notes can only contain letters, numbers, spaces and specific special characters: -\'."',
      )
      .optional(),
  })
  .strict();