Module: Oddb2xml::WeledaSL

Defined in:
lib/oddb2xml/weleda_sl.rb

Overview

Recovers the SL reimbursement flag and the public price for the Swiss “Kapitel 70” complementary medicines (Homöopathika / Anthroposophika / Phytotherapeutika) that are not present in the BAG FHIR NDJSON feed.

These products were historically scraped from the BAG “varia” page (chapter_70_hack). That page became a JavaScript SPA (issue #118) and the FHIR feed only covers a part of the catalogue, so the magistral Weleda products (GTIN prefix 7611916…) arrive via ZurRose with no SL flag and a zeroed/absent public price (issue #117/#121).

Two data files close the gap. They live in github.com/zdavatz/oddb2xml_files (downloaded at runtime, so they can be refreshed without a gem release) with a bundled copy under data/ as an offline fallback:

* weleda_arzneimittel.csv   GTIN -> abgabekategorie (the "… / SL" flag)
                            and csl (= Pharma-Gruppen-Code).
* bag_sl_group_prices.csv   Pharma-Gruppen-Code -> public price (CHF,
                            incl. MWST). Extracted from the BAG SL
                            definition PDF "Homoeopathica, Anthroposophica,
                            Allergene.pdf" — the authoritative price source.
* wala_arzneimittel.csv     The same gap for WALA products (GTIN prefix
                            7640187…). Different layout (";"-separated,
                            BOM): a row is SL when it carries a CSL-Code
                            (Kapitel-70.01 group code) and the public
                            package price is given *inline* in the
                            "CSL 70.01." column — already multiplied for
                            the pack size (the multiplier lives only in the
                            galenic-form text, e.g. "Solutio ad inj.
                            10 x 1 ml"), so it is used verbatim rather than
                            re-joined against bag_sl_group_prices.csv.

Weleda join: GTIN -> csl -> price. The csl may carry a package multiplier in the form “N x <code>” (e.g. “8x2070631”), meaning the package holds N units priced at <code> each, so the public price is N * price.

The FHIR feed always wins: this enrichment is only applied to GTINs that are absent from the NDJSON (see Builder#build_artikelstamm).

Constant Summary collapse

DATA_DIR =
File.expand_path(File.join(__dir__, "..", "..", "data"))

Class Method Summary collapse

Class Method Details

.build_map(csv_string, prices) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/oddb2xml/weleda_sl.rb', line 99

def build_map(csv_string, prices)
  map = {}
  return map if csv_string.nil? || csv_string.strip.empty?
  CSV.parse(csv_string, headers: true) do |row|
    next unless (row["abgabekategorie"].to_s =~ /\bSL\b/)
    gtin = row["ean"].to_s.strip.rjust(13, "0")
    next unless gtin =~ /\A\d{13}\z/
    price = resolve_price(row["csl"], prices)
    map[gtin] = {
      sl: true,
      price: price,
      csl: row["csl"].to_s.strip,
      abgabe: row["abgabekategorie"].to_s.strip
    }
  end
  map
end

.build_wala_map(csv_string) ⇒ Object

WALA layout: “;”-separated, BOM, header columns carry trailing spaces. A row is an SL product when it has a CSL-Code (Kapitel-70.01 group code); the public package price is taken verbatim from the inline “CSL 70.01.” column (already multiplied for the pack size). Keyed by 13-digit GTIN.



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/oddb2xml/weleda_sl.rb', line 121

def build_wala_map(csv_string)
  map = {}
  return map if csv_string.nil? || csv_string.strip.empty?
  content = csv_string.sub("", "")
  table = CSV.parse(content, headers: true, col_sep: ";")
  col = {}
  table.headers.compact.each { |h| col[h.to_s.strip] = h }
  table.each do |row|
    csl = row[col["CSL-Code*"]].to_s.strip
    next if csl.empty? # no group code => not an SL product
    gtin = row[col["EAN-Code"]].to_s.strip.rjust(13, "0")
    next unless gtin =~ /\A\d{13}\z/
    raw_price = row[col["CSL 70.01."]].to_s.strip
    next if raw_price.empty?
    map[gtin] = {
      sl: true,
      price: sprintf("%.2f", raw_price.tr(",", ".").to_f),
      csl: csl,
      abgabe: row[col["KAT"]].to_s.strip
    }
  end
  map
end

.load(options = {}) ⇒ Object

Returns a Hash keyed by the 13-digit GTIN (String, zero-padded):

"7611916162404" => { sl: true, price: "26.95", csl: "2069591", abgabe: "FM / SL" }

Only rows carrying a “/ SL” Abgabekategorie are included. Returns {} if the data cannot be obtained (never raises — the rest of the build must proceed).



52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/oddb2xml/weleda_sl.rb', line 52

def load(options = {})
  prices = parse_prices(source(BagSlGroupPricesDownloader, options, "bag_sl_group_prices.csv"))
  map = build_map(source(WeledaDownloader, options, "weleda_arzneimittel.csv"), prices)
  weleda_size = map.size
  build_wala_map(source(WalaDownloader, options, "wala_arzneimittel.csv")).each do |gtin, entry|
    map[gtin] ||= entry # Weleda wins on the (unlikely) GTIN collision
  end
  Oddb2xml.log "WeledaSL: #{map.size} SL products with prices loaded " \
    "(Weleda #{weleda_size}, WALA #{map.size - weleda_size})"
  map
rescue => error
  Oddb2xml.log "WeledaSL: disabled (#{error.class}: #{error.message})"
  {}
end

.parse_prices(csv_string) ⇒ Object

Pharma-Gruppen-Code => unit price (String, “NN.NN”).



88
89
90
91
92
93
94
95
96
97
# File 'lib/oddb2xml/weleda_sl.rb', line 88

def parse_prices(csv_string)
  prices = {}
  return prices if csv_string.nil? || csv_string.strip.empty?
  CSV.parse(csv_string, headers: true) do |row|
    code = row["pharma_group_code"].to_s.strip
    price = row["price_chf_incl_vat"].to_s.strip
    prices[code] = price unless code.empty? || price.empty?
  end
  prices
end

.resolve_price(csl, prices) ⇒ Object

csl is either “<code>” or “<N> x <code>” (the package multiplier). Returns the public price as a “NN.NN” String, or nil when it cannot be resolved.



147
148
149
150
151
152
153
154
155
156
# File 'lib/oddb2xml/weleda_sl.rb', line 147

def resolve_price(csl, prices)
  csl = csl.to_s.strip
  return nil if csl.empty?
  m = csl.match(/\A(?:(\d+)\s*[x×]\s*)?(\d{7})\z/i)
  return nil unless m
  multiplier = (m[1] || "1").to_i
  base = prices[m[2]]
  return nil unless base
  sprintf("%.2f", base.to_f * multiplier)
end

.source(downloader_class, options, basename) ⇒ Object

Download the file from oddb2xml_files; fall back to the bundled copy under data/ when the download is unavailable (e.g. an allow-list proxy blocks raw.githubusercontent.com).



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/oddb2xml/weleda_sl.rb', line 70

def source(downloader_class, options, basename)
  content = nil
  begin
    content = downloader_class.new(options).download
  rescue => error
    Oddb2xml.log "WeledaSL: download of #{basename} failed (#{error.class}: #{error.message})"
  end
  if content.nil? || content.to_s.strip.empty?
    bundled = File.join(DATA_DIR, basename)
    if File.exist?(bundled)
      Oddb2xml.log "WeledaSL: using bundled #{basename}"
      content = File.read(bundled)
    end
  end
  content
end