Skip to main content

Manual Quotations

A quotation adapter for human-reviewed price negotiation: an admin proposes a price via makeQuotationProposal, and the resulting quote expires after 1 hour by default.

Included in Base Preset

Registered automatically by registerBasePlugins() and registerAllPlugins().

If you register plugins individually instead of using a preset:

import { pluginRegistry } from '@unchainedshop/core';
import { ManualOfferingPlugin } from '@unchainedshop/plugins/quotations/manual';

pluginRegistry.register(ManualOfferingPlugin);

Adapter Details​

PropertyValue
Keyshop.unchained.quotations.manual
Order Index0
ActivationAll products (isActivatedFor returns true)
Sourcequotations/manual

Behavior​

quote() derives the proposal from the persisted quotation context:

  • price: taken from quotation.context.price (minor units of the quotation's currency), i.e. whatever was passed to makeQuotationProposal
  • expires: quotation.context.expires if set, otherwise now + 1 hour

Usage​

Request a Quotation​

mutation RequestQuotation {
requestQuotation(
productId: "product-id"
configuration: [
{ key: "quantity", value: "100" }
{ key: "notes", value: "Bulk order for corporate event" }
]
) {
_id
status
expires
}
}

Admin: Make a Proposal​

quotationContext is stored on the quotation and read by quote():

mutation MakeProposal {
makeQuotationProposal(
quotationId: "quotation-id"
quotationContext: { price: 8999 }
) {
_id
status
}
}

Verify a Request​

mutation VerifyQuotation {
verifyQuotation(quotationId: "quotation-id") {
_id
status
}
}

Query Quotations​

query MyQuotations {
me {
quotations {
_id
status
product {
texts { title }
}
expires
quotationNumber
}
}
}

Quotation States​

StatusDescription
REQUESTEDRequest for proposal
PROCESSINGAwaiting offer
PROPOSEDA price has been proposed
FULFILLEDQuotation has been accepted and used
REJECTEDQuotation was rejected

Custom Quotation Logic​

Use the registerQuotation factory:

import { registerQuotation } from '@unchainedshop/core';

registerQuotation({
adapterId: 'bulk-pricing',
quote: async ({ quotation }) => {
if (!quotation) return {};
const quantity =
Number(quotation.configuration?.find((entry) => entry.key === 'quantity')?.value) || 1;
const basePrice = await catalogPriceFor(quotation.productId, quotation.currencyCode);
const discount = quantity > 100 ? 0.15 : quantity > 50 ? 0.1 : 0.05;

return {
expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
price: Math.round(basePrice * (1 - discount)), // discounted unit price
};
},
});