All files / src/suppliers SupplierLibertySci.ts

96.49% Statements 55/57
85.71% Branches 6/7
100% Functions 1/1
96.49% Lines 55/57

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104  1x   1x 1x 1x 1x   1x                                         1x 1x 1x 1x   1x 1x   1x 1x   1x 1x   1x 1x   1x                           1x 1x 3x 3x 3x 3x   3x 3x 3x       3x 3x 3x 3x 3x 3x 3x 1x 1x   2x   2x 2x 2x 2x 2x 2x 2x 2x 2x 3x 3x 2x 2x 3x   3x 3x 1x  
import { ProductBuilder } from '@/utils/ProductBuilder';
import { SupplierBaseWoocommerce } from './SupplierBaseWoocommerce';
 
// Matches a molecular/formula weight label and its value in a description, e.g. "F.W. 122.55",
// "M.W.: 98.1", "Molecular Weight 140.22". The numeric value is captured in group 1.
const MOLE_WEIGHT_REGEX =
  /(?:f\.?\s*w\.?|m\.?\s*w\.?|molecular\s+weight|formula\s+weight)\s*[:.=]?\s*(\d+(?:\.\d+)?)/i;
 
/**
 * Supplier class for LibbertySci, a chemical supplier using the WooCommerce platform.
 * Implements product fetching and parsing functionality specific to LibertySci's website.
 *
 * @example
 * ```typescript
 * const supplier = new SupplierLibertySci();
 *
 * // Iterate over all products
 * for await (const product of supplier) {
 *   console.log(product.name, product.cas, product.price);
 * }
 *
 * // Search for specific products
 * const products = await supplier.search("acetone");
 * console.log(`Found ${products.length} products`);
 * ```
 *
 * @see https://www.libertysci.com/
 * @see https://www.libertysci.com/wp-json/wc/store/v1/products
 * @source
 */
export class SupplierLibertySci extends SupplierBaseWoocommerce implements ISupplier {
  // The display name of the supplier.
  public static readonly supplierName: string = 'LibertySci';
 
  // Shipping scope for LibertySci
  public static readonly shipping: ShippingRange = 'worldwide';
 
  // The country code of the supplier.
  public static readonly country: CountryCode = 'US';
 
  // The base URL for the supplier's website.
  public static readonly baseURL: string = 'https://libertysci.com';
 
  // The payment methods accepted by the supplier.
  public static readonly paymentMethods: PaymentMethod[] = ['mastercard', 'visa'];
 
  /**
   * Builds the base WooCommerce product builders, then enriches each with the chemical formula and
   * molecular weight that LibertySci embeds in the product description (e.g. "KClO<sub>3</sub>,
   * F.W. 122.55"). The other description-derived fields (CAS, image, thumbnail, rating, review
   * count) are already handled by the base class.
   *
   * @param results - Array of WooCommerce product items from the search response
   * @returns Array of ProductBuilder instances with formula and molecular weight populated
   * @example
   * ```typescript
   * const builders = this.initProductBuilders(items);
   * // builders[0] now carries e.g. formula "KClO₃" and moleweight 122.55
   * ```
   * @source
   */
  protected initProductBuilders(
    results: WooCommerceSearchResponseItem[],
  ): ProductBuilder<Product>[] {
    // The base maps items to builders 1:1 (no filtering), so indexes line up with `results`.
    const builders = super.initProductBuilders(results);
 
    builders.forEach((builder, index) => {
      const description = results[index]?.description;
      if (typeof description !== 'string' || description.length === 0) {
        return;
      }
 
      // LibertySci puts the formula immediately before a formula/molecular weight label
      // (e.g. "NaOH, F.W. 40.00"). Anchor on that label: scanning the whole description
      // instead mistakes the "F.W." label itself for a formula (F·W — both real element
      // symbols) or a code like "UN1824" for one. Without a label (e.g. solutions) there's
      // no reliable formula to read, so leave it unset.
      const weightMatch = description.match(MOLE_WEIGHT_REGEX);
      if (!weightMatch) {
        return;
      }
 
      builder.setMoleweight(weightMatch[1]);
 
      // The formula is the last non-empty segment before the label. setFormula runs
      // findFormulaInHtml internally, so a tagged formula like "KClO<sub>3</sub>" is
      // rendered to "KClO₃" and a clean one like "NaOH" is stored as-is.
      const formulaSegment = description
        .slice(0, weightMatch.index)
        .split(/<br\s*\/?>|\r?\n/i)
        .map((segment) => segment.trim())
        .filter(Boolean)
        .pop()
        ?.replace(/[\s,;:·]+$/, '');
      if (formulaSegment) {
        builder.setFormula(formulaSegment);
      }
    });
 
    return builders;
  }
}