@metron-labs/quote-react 0.1.61 → 0.1.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,8 @@ const FIXED_PRODUCTS = {
18
18
  architrave: { "architrave-ogee": "134 primed mdf ogee", "architrave-pencil-round": "1896", "architrave-square-edge": "118 primed mdf square", "architrave-torus": "1531" },
19
19
  // Board keys come from the board type; see fixedProductSearch.
20
20
  plasterboard: { standard: "1647", moisture: "11 moisture plasterboard", fire: "14 fire plasterboard", sound: "1769", screws: "131160", tape: "1766", adhesive: "108439" },
21
+ paint: { "paint-emulsion": "157217" },
22
+ "concrete-aggregate": { cement: "1762 cement", "sharp-sand": "563", gravel: "571", mot: "569" },
21
23
  insulation: { "insulation-floor": "152187", "insulation-ground": "442", "insulation-wall": "147", "insulation-roof": "442", "insulation-loft": "152180" },
22
24
  timber: { studs: "1213", joists: "208", battens: "1729" },
23
25
  fencing: {
@@ -102,6 +102,24 @@ const packItemsForRequestedUnit = (item, requestedUnit) => {
102
102
  }
103
103
  return null;
104
104
  };
105
+ // A tin, bag or tub priced per item usually names its size, e.g. "Vinyl Matt Emulsion - 5L" or
106
+ // "Sharp Sand Bulk Bag (800kg)"; a request in litres or kilograms is turned into whole packs from
107
+ // that. Only used when the catalogue gives no volume or weight itself.
108
+ const packSizeFromDescription = (item, measure) => {
109
+ const pattern = measure === "volume" ? /(\d+(?:[.,]\d+)?)\s*(ml|l|ltr|ltrs|litres?|liters?)\b/i : /(\d+(?:[.,]\d+)?)\s*(kg|kgs|tonnes?)\b/i;
110
+ const match = item.description.match(pattern);
111
+ if (!match)
112
+ return null;
113
+ const amount = Number.parseFloat(match[1].replace(",", "."));
114
+ if (!Number.isFinite(amount) || amount <= 0)
115
+ return null;
116
+ const unit = match[2].toLowerCase();
117
+ if (measure === "volume")
118
+ return { ml: unit === "ml" ? amount : amount * 1000 };
119
+ return { g: unit.startsWith("tonne") ? amount * 1000000 : amount * 1000 };
120
+ };
121
+ const catalogueVolumeTypes = ["volume_from_description", "same_unit_volume"];
122
+ const catalogueWeightTypes = ["weight_from_description", "mass_from_pack", "same_unit_mass"];
105
123
  // Product exports often omit structured coverage but include it verbatim, e.g. "13.89m2/Roll".
106
124
  // This is intentionally narrow: it must name the same sell unit as the catalogue row.
107
125
  const coverageFromDescription = (item, requestedUnit, sellUnit) => {
@@ -142,6 +160,34 @@ export function getPricingBreakdown(item, options) {
142
160
  });
143
161
  if (!Number.isFinite(requestedAmount) || requestedAmount <= 0)
144
162
  return review("Quantity needs confirmation.");
163
+ const requestVolumeMl = volumeInMl(requestedAmount, requestedUnit);
164
+ const requestWeightG = weightInG(requestedAmount, requestedUnit);
165
+ const packFromDescription = sameUnit(requestedUnit, sellUnit)
166
+ ? null
167
+ : requestVolumeMl != null && !catalogueVolumeTypes.includes(item.conversionType ?? "")
168
+ ? packSizeFromDescription(item, "volume")
169
+ : requestWeightG != null && !catalogueWeightTypes.includes(item.conversionType ?? "")
170
+ ? packSizeFromDescription(item, "weight")
171
+ : null;
172
+ const requested = packFromDescription?.ml != null ? requestVolumeMl : requestWeightG;
173
+ const packAmount = packFromDescription?.ml ?? packFromDescription?.g ?? null;
174
+ if (requested != null && packAmount != null) {
175
+ const priceUnit = canonicalUnit(item.uoi) || sellUnit;
176
+ const packLabel = packFromDescription?.ml != null ? `${packAmount / 1000} l` : `${packAmount / 1000} kg`;
177
+ const required = hasOverride ? overriddenSellUnits : Math.ceil((requested * (1 + wasteFactor)) / packAmount - 1e-9);
178
+ return {
179
+ requestedAmount,
180
+ requestedUnit,
181
+ salesUnitSize: 1,
182
+ sellUnit: priceUnit,
183
+ calculatedSellUnits: required,
184
+ requiredSellUnits: required,
185
+ unitPrice: item.price,
186
+ lineTotal: item.price != null ? item.price * required : null,
187
+ calculation: `${requestedAmount} ${requestedUnit}${wasteFactor > 0 ? ` x ${1 + wasteFactor}` : ""} / ${packLabel} per ${priceUnit} (from product description) = ${required} ${priceUnit}`,
188
+ costingMode: "derived",
189
+ };
190
+ }
145
191
  // "sales_unit" means one listed-price unit, whatever it holds: 1 sales_unit of a box of
146
192
  // 1,000 screws priced "per Each" is one box at the listed price. The estimators ask for it
147
193
  // when they have already counted packs (see estimateRequestUnit).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metron-labs/quote-react",
3
- "version": "0.1.61",
3
+ "version": "0.1.63",
4
4
  "description": "Metron Labs quote and estimating calculator components for React",
5
5
  "license": "UNLICENSED",
6
6
  "sideEffects": [