All files / src/suppliers SupplierDiyChemicals.ts

100% Statements 49/49
92.85% Branches 13/14
100% Functions 3/3
100% Lines 49/49

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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 1501x   1x   1x                                         1x 1x 1x                     1x 1x   1x 1x   1x 1x   1x 1x   1x 1x 1x 1x 1x 1x 1x   1x                                   1x 1x 14x 13x 13x   1x                                       1x 1x 3x 3x   1x                                                 1x 1x 10x 10x 10x 10x 10x 10x   10x 10x 3x 3x 10x 10x 10x 1x  
import { findPdfHref, stripPdfLink } from '@/helpers/utils';
import type { ProductBuilder } from '@/utils/ProductBuilder';
import { SupplierBaseWoocommerce } from './SupplierBaseWoocommerce';
 
/**
 * Supplier class for DIY Chemicals, a chemical supplier using the WooCommerce platform.
 * Implements product fetching and parsing functionality specific to DIY Chemicals's website.
 *
 * @example
 * ```typescript
 * const supplier = new SupplierDiyChemicals();
 *
 * // 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://diychemicals.com/
 * @see https://diychemicals.com/wp-json/wc/store/v1/products
 * @source
 */
export class SupplierDiyChemicals extends SupplierBaseWoocommerce implements ISupplier {
  /**
   * The display name of the supplier.
   * Used for identifying the supplier in product listings and user interfaces.
   *
   * @example
   * ```typescript
   * const supplier = new SupplierDiyChemicals();
   * console.log(`Products from ${supplier.supplierName}`);
   * // Output: "Products from DIY Chemicals"
   * ```
   * @source
   */
  public static readonly supplierName: string = 'DIY Chemicals';
 
  // The base URL for the supplier's website.
  public static readonly baseURL: string = 'https://diychemicals.com';
 
  // Shipping scope for DIY Chemicals
  public static readonly shipping: ShippingRange = 'domestic';
 
  // The country code of the supplier.
  public static readonly country: CountryCode = 'US';
 
  // The payment methods accepted by the supplier.
  public static readonly paymentMethods: PaymentMethod[] = [
    'mastercard',
    'visa',
    'paypal',
    'amazon',
  ];
 
  /**
   * DIY Chemicals stores each variation's pack size as a slug under
   * `variations[].attributes[].value` (e.g. `"4-gallon-pack"`), which
   * `parseQuantity` can't read since it has no whitespace between the number
   * and the unit. The human-readable form lives instead on the product-level
   * `attributes[].terms[].name` (e.g. `"4 Gallon Pack"`). Surface every term
   * name as a quantity-parsing candidate so the base class can extract
   * quantity/UoM via its existing `parseQuantity` pipeline. `parseQuantity`
   * ignores strings that don't match a quantity pattern, so unrelated
   * attribute terms are harmless.
   * @param item - The raw WooCommerce search response item
   * @returns Term names from every product attribute, as quantity candidates
   * @example
   * ```typescript
   * // input:  { attributes: [{ terms: [{ name: "4 Gallon Pack" }] }] }
   * // output: ["4 Gallon Pack"]
   * ```
   * @source
   */
  protected getAdditionalQuantityStrings(item: WooCommerceSearchResponseItem): string[] {
    if (!Array.isArray(item.attributes)) return [];
    return item.attributes.flatMap((attr) => attr.terms?.map((term) => term.name) ?? []);
  }
 
  /**
   * DIY Chemicals's per-variant `name` is just the flat parent product name
   * — identical across every size/option of a product (e.g. every "Sodium
   * Metasilicate" variant shares that one name), so the base class's default
   * leaves every variant in the results table indistinguishable. The Store
   * API's `variation` field carries the actual attribute combination
   * instead (e.g. "Size: 40 Pound Pail (40lbs), Item Form: Sodium
   * Metasilicate (Anhydrous)"), so prefer that when present.
   * @param data - The raw variant detail data
   * @returns The variant's attribute-combination string, or the base
   * class's default (the product name) when `variation` is empty
   * @example
   * ```typescript
   * this.getVariantTitle({
   *   name: 'Sodium Metasilicate',
   *   variation: 'Size: 40 Pound Pail (40lbs), Item Form: Sodium Metasilicate (Anhydrous)',
   * });
   * // "Size: 40 Pound Pail (40lbs), Item Form: Sodium Metasilicate (Anhydrous)"
   * ```
   * @source
   */
  protected getVariantTitle(data: WooCommerceSearchResponseItem): string {
    return data.variation || super.getVariantTitle(data);
  }
 
  /**
   * DIY Chemicals embeds its SDS link directly in the product description as
   * an `<a href="….pdf">DOWNLOAD SDS</a>` anchor (usually at the start,
   * occasionally at the end), rather than exposing it as its own field.
   * Left alone, the base class's plain `htmlToAscii` conversion strips the
   * markup and drops the `href`, leaving the dead text "DOWNLOAD SDS" sitting
   * in the description with no way to reach the actual document. Runs after
   * the base class builds its builders, extracting the link's URL into
   * `sdsUrl` via `findPdfHref` and removing the anchor from the description
   * text via `stripPdfLink`. `findPdfHref` matches the first PDF link in the
   * description regardless of what it's for, so the match is additionally
   * required to mention "sds" (every real example's URL contains "MSDS")
   * before being trusted as the SDS — an unrelated PDF (a spec sheet, say)
   * is left alone rather than misfiled as the safety data sheet. Products
   * with no matching link are returned untouched.
   * @param results - Array of WooCommerce search response items
   * @returns Product builders with the SDS link moved out of the description
   * and into `sdsUrl`, where present
   * @example
   * ```typescript
   * // input description: '<p><a href="…MSDS….pdf">DOWNLOAD SDS</a></p><h1>EDTA 40%</h1>...'
   * // output: builder.dump().sdsUrl -> "…MSDS….pdf"
   * // output: builder.dump().description -> "EDTA 40%\n..." (no "DOWNLOAD SDS" text)
   * ```
   * @source
   */
  protected initProductBuilders(
    results: WooCommerceSearchResponseItem[],
  ): ProductBuilder<Product>[] {
    const builders = super.initProductBuilders(results);
    return builders.map((builder, index) => {
      const description = results[index]?.description;
      if (typeof description !== 'string') return builder;
 
      const sdsUrl = findPdfHref(description);
      if (sdsUrl && /sds/i.test(sdsUrl)) {
        builder.setDescription(stripPdfLink(description)).setSDSUrl(sdsUrl);
      }
      return builder;
    });
  }
}