oddb2xml 3.0.29 → 3.0.30

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4602599ed50439225ff6ce76000f8441a9b4963b02538adf3f46d6728beff6cc
4
- data.tar.gz: 55f34b946cc7a0e75435459aa90169e23719ac44c4bbfdf1ba3d63c3162f894d
3
+ metadata.gz: dd5aba5cd43be1930a5499b102b5a1921454d422de9aa13de227e92ad1b5cae6
4
+ data.tar.gz: 1861fc5dba9e46179aaa4f69e64fa6e10288aeafff677ec591156926a54e8633
5
5
  SHA512:
6
- metadata.gz: cf98f9144a53cd62be8599168423c4c6d560b8474dc015d5281eccb84b2f80de6d46e3765b7ab96cc6271822361ea0a4a7b1f00bae1908e16f94f8fb77006908
7
- data.tar.gz: 7ed225d5f4cd6f3c3293913cb40e01430839968d34e15551a2192a1f9069343dd98f7448179a0e433bc6485d8810510904add62632e6e5ddf6d03d50b8f41dc8
6
+ metadata.gz: eb67bef331b97a7f3b347d342c9c2c8128fe9d53154363b89d3868d97d61d2d6469568aa78fb597129f33687af68c2ef38ecb227a6e67d807b8e06df220e4694
7
+ data.tar.gz: b4835b7045e30b1e24a84afd96c499db2b09fe0ec0175ea8b8e82adfe9a7b4a096237e6be6879cb9636f9608a0161346dda37b1606abf38d62762d858375b056
data/CLAUDE.md CHANGED
@@ -55,6 +55,8 @@ The system follows a **download → extract → build → compress** pipeline:
55
55
 
56
56
  10. **Weleda / Kapitel-70 SL recovery** (`lib/oddb2xml/weleda_sl.rb`, 3.0.21 onwards) — Recovers the SL flag and public price for chapter-70 complementary medicines that are **missing from the FHIR feed** (the partial-replacement gap left by the dead chapter_70_hack, issue #118/#121). Many are magistral Weleda preparations with a `7611916…` trade GTIN that arrive only via ZurRose — with no SL flag and a blanked Publikumspreis (issue #117). `WeledaSL.load` joins two CSVs (downloaded at runtime from `github.com/zdavatz/oddb2xml_files` via `WeledaDownloader` / `BagSlGroupPricesDownloader`, bundled fallback copies under `data/`): `weleda_arzneimittel.csv` (GTIN → `abgabekategorie` SL flag + `csl` = **Pharma-Gruppen-Code**) and `bag_sl_group_prices.csv` (Pharma-Gruppen-Code → public price). `weleda_arzneimittel.csv` is **regenerated** by the Rust tool `weleda_scraper/` in the `oddb2xml_files` repo (`scraper --update weleda`, prompts for the medical.weleda.ch `PHPSESSID` cookie — never stored): it walks the paginated Arzneimittel-Verzeichnis listing + per-product detail pages and rewrites the CSV with exactly the currently-listed products (delisted rows dropped), preserving the on-disk format (column order, UTF-8, CRLF, quote-when-necessary, sorted by `id`) so oddb2xml reads it unchanged. The price table is extracted **offline** from the BAG SL definition PDF *"Homoeopathica, Anthroposophica, Allergene"* via `tools/generate_bag_sl_group_prices.rb` (uses system `pdftotext`; **no runtime PDF gem** — `pdf-reader`'s `afm` dep now needs Ruby ≥ 3.2, which would break the gem's Ruby floor). The join is **GTIN → csl → price**, honouring an `N x <code>` package multiplier (price = N × group price). Produces `gtin => {sl:, price:, csl:, abgabe:}` (SL rows only; ~515 priced on the live feed). **WALA products (3.0.22 onwards):** a third runtime CSV `wala_arzneimittel.csv` (GTIN prefix `7640187…`, `WalaDownloader`, bundled fallback) is merged into the same map via `WeledaSL.build_wala_map`. Its layout differs: `;`-separated with a BOM, no `/ SL` column (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 appears only in the galenic-form text, e.g. `Solutio ad inj. 10 x 1 ml`), so it is taken **verbatim** rather than re-joined against `bag_sl_group_prices.csv` (which holds the per-unit price and would yield 1/10 of the package price for ~120 multi-unit packs). 320 WALA SL products on the live file; Weleda wins on the (unlikely) GTIN collision. `Builder#build_artikelstamm` consumes it (CLI sets `builder.weleda_sl` only for `--artikelstamm`): for any GTIN **absent from the FHIR NDJSON** it emits `<SL_ENTRY>true</SL_ENTRY>` and `<PPUB>` from the BAG group price, mirroring the old chapter-70 behaviour (`PHARMATYPE "P"`). **The FHIR/ZurRose price always wins** — the group price only fills a gap; a zeroed ZurRose `"0.00"` pub price is treated as absent so the gap-fill can apply. Match is **by GTIN only** (no pharmacode); the Swissmedic dispensing category is untouched (still from `Swissmedic_Packungen.xlsx`). The Artikelstamm output gets `<SL_ENTRY>` + `<PPUB>`; for the `-e`/`--extended` and `-b`/`--firstbase` product feeds the BAG public price is also added to `oddb_article.xml` as an `<ARTPRI><PTYP>BAGPUB</PTYP>` entry (the raw, often-blanked `ZURROSEPUB` is preserved alongside it) — `build_article`, gated by the CLI loading `weleda_sl` when `extended || firstbase || artikelstamm`. See GitHub issue #121.
57
57
 
58
+ 11. **Rogger name preferences** (`lib/oddb2xml/rogger_names.rb`, `-r`/`--rogger`, 3.0.30 onwards) — Replaces the German article description (`desc_de`) with the preferred name from the **"Rogger Mediliste"** for every GTIN on that list. The list collects the name conflicts Frau Rogger (Vitabyte/Zur Rose, task #OX-5985-1594) reports between the Spirig/Galexis names and what Vitabyte shows from raw Refdata; most entries are the issue-#112 Refdata bugs with their corrected names (METOJECT truncation, CETIRIZIN missing strength, ATOVAQUON missing 2nd combo dose, MOUNJARO missing pen volume, RINVOQ `Retardtabletten`), plus two not (yet) catalogued in `refdata_cleanup.rb`: GABAPENTIN Spirig HC Kaps (IKSNR 66594, strength doubled as a glued brand token `Spirig HC 100mg Kaps 100 mg`) and MELATONIN Spirig HC 100 Stk (redundant `retard` before `Ret Tabl`). Source of truth is the shared Google Sheet "Rogger Mediliste" (link-shared read-only); `RoggerDownloader` fetches its CSV export (`GTIN,Mediname`) **directly** (`docs.google.com/spreadsheets/d/<id>/export?format=csv&gid=0`), so sheet edits reach the feeds without any release step, with a bundled fallback `data/rogger_liste.csv` (refresh at release time). `RoggerNames.load` returns `gtin => name` (never raises; UTF-8-forced because the list carries `µg`; `rogger_csv?` rejects non-CSV responses such as a Google sign-in page if the sheet ever loses its link-sharing, engaging the fallback). The override runs as the **last step of `Builder#apply_refdata_description_cleanups!`** (`apply_rogger_name_overrides!`), so it sees and wins over the issue-#112 cleanups; the list is German-only, FR/IT descriptions are untouched. CLI wires `builder.rogger_names` only when `--rogger` is set (default off, all feeds unchanged without it).
59
+
58
60
  ### Key data identifiers
59
61
  - **GTIN/EAN13**: Primary article identifier (13-digit barcode)
60
62
  - **Pharmacode**: Swiss pharmacy code
@@ -68,7 +70,7 @@ YAML files in `data/` provide manual overrides and mappings: `article_overrides.
68
70
 
69
71
  These scripts run the public download server at `https://mediupdatexml.oddb.org` (Apache on this host) and are **not** part of the gem itself.
70
72
 
71
- - **`run_oddb2xml.sh`** — nightly build driver (cron: `0 1 * * * zdavatz`). Downloads the upstream sources **once**, then builds the `-b`/firstbase feed at price increments `45/50/55` plus `default` (no increment) into `$OUT_DIR` (`/home/zdavatz/oddb2xml`, one subdir each). The shared `downloads/` cache and transient zip live in `$BUILD_DIR` (`<OUT_DIR>-build`), **outside** `$OUT_DIR` so the transfer never uploads the multi-hundred-MB cache. Final step ("2b") regenerates the landing page. Each `oddb2xml` invocation is wrapped in `run_with_retry` (default **3 attempts, 120 s apart**, tunable via `ODDB2XML_RETRIES`/`ODDB2XML_RETRY_DELAY`): a transient upstream download failure (e.g. Swissmedic resetting the connection, `Errno::ECONNRESET`) previously aborted the whole `set -e` run 14 s in and left the feeds a day stale, so it now retries before giving up; a genuine repeated failure still stops the run. **Firstbase (GS1 NONPHARMA) last-good fallback (3.0.29 onwards):** the GS1 `GetFirstbaseHealthcare` export (`id.gs1.ch/01/07612345000961` → `apitools.gs1.ch`) has been answering `403 - Forbidden`, which blanked `firstbase.csv` and dropped **every** NONPHARMA article from the `-b` feed (landing page then showed `NONPHARMA = 0 − 1 = −1`). The script keeps the last successful `firstbase.csv` in a persistent cache `$FIRSTBASE_CACHE` (default `<OUT_DIR>-state/firstbase.csv`) **outside** `$BUILD_DIR` so it survives the nightly `rm -rf`, seeds it into `downloads/` before the build, and refreshes it after a successful download. The gem side (`FirstbaseDownloader#download`, rewritten in 3.0.29) makes the seed usable: it still attempts the live GS1 fetch on the first (downloading) build, but only overwrites `firstbase.csv` when the response is a real non-empty CSV (`firstbase_csv?` rejects HTML/`403 - Forbidden`/empty bodies and open-uri exceptions), otherwise it **keeps the existing seeded file** instead of the old `"w+"` truncate-to-zero. A recovered GS1 therefore refreshes the data automatically; while GS1 is down the feed serves yesterday's (last-good) NONPHARMA rather than nothing. `generate_index_html.sh` also guards the NONPHARMA count so an empty CSV renders `—` (not `−1`).
73
+ - **`run_oddb2xml.sh`** — nightly build driver (cron: `0 1 * * * zdavatz`). Downloads the upstream sources **once**, then builds the `-b`/firstbase feed at price increments `45/50/55` plus `default` (no increment) into `$OUT_DIR` (`/home/zdavatz/oddb2xml`, one subdir each). The shared `downloads/` cache and transient zip live in `$BUILD_DIR` (`<OUT_DIR>-build`), **outside** `$OUT_DIR` so the transfer never uploads the multi-hundred-MB cache. Final step ("2b") regenerates the landing page. Each `oddb2xml` invocation is wrapped in `run_with_retry` (default **3 attempts, 120 s apart**, tunable via `ODDB2XML_RETRIES`/`ODDB2XML_RETRY_DELAY`): a transient upstream download failure (e.g. Swissmedic resetting the connection, `Errno::ECONNRESET`) previously aborted the whole `set -e` run 14 s in and left the feeds a day stale, so it now retries before giving up; a genuine repeated failure still stops the run. **Firstbase (GS1 NONPHARMA) last-good fallback (3.0.29 onwards):** the GS1 `GetFirstbaseHealthcare` export (`id.gs1.ch/01/07612345000961` → `apitools.gs1.ch`) has been answering `403 - Forbidden`, which blanked `firstbase.csv` and dropped **every** NONPHARMA article from the `-b` feed (landing page then showed `NONPHARMA = 0 − 1 = −1`). The script keeps the last successful `firstbase.csv` in a persistent cache `$FIRSTBASE_CACHE` (default `<OUT_DIR>-state/firstbase.csv`) **outside** `$BUILD_DIR` so it survives the nightly `rm -rf`, seeds it into `downloads/` before the build, and refreshes it after a successful download. The gem side (`FirstbaseDownloader#download`, rewritten in 3.0.29) makes the seed usable: it still attempts the live GS1 fetch on the first (downloading) build, but only overwrites `firstbase.csv` when the response is a real non-empty CSV (`firstbase_csv?` rejects HTML/`403 - Forbidden`/empty bodies and open-uri exceptions), otherwise it **keeps the existing seeded file** instead of the old `"w+"` truncate-to-zero. A recovered GS1 therefore refreshes the data automatically; while GS1 is down the feed serves yesterday's (last-good) NONPHARMA rather than nothing. `generate_index_html.sh` also guards the NONPHARMA count so an empty CSV renders `—` (not `−1`). **ZurRose/pillbox last-good seed + first-build `--skip-download` (2026-07-07):** the actual reason the nightly feed went stale after 2026-07-03 was **not** firstbase (a firstbase failure is non-fatal) but **ZurRose**: `pillbox.oddb.org:80` started refusing connections around 01:00 every night from 2026-07-04 on, so `ZurroseDownloader` wrote a 0-byte `transfer.zip`, `unzip` produced no `transfer.dat`, and `downloader.rb:221` raised `Errno::ENOENT`, aborting the whole `set -e` run (all three retries hit the same dead pillbox). Fix is **script-only, no gem release**: `get_transfer.sh` (cron 00:30) already mirrors the ZurRose zip locally at `$GET_TRANSFER_ZIP` (`/home/zdavatz/software/get_transfer/TRANSFER.ZIP`), so the **first** `build_one` now runs a `first_build_attempt` helper that (a) `seed_downloads` — resets `downloads/` to contain only that seeded `transfer.zip` — then (b) runs `oddb2xml --skip-download -b -c zip`. The `--skip-download` flag makes `Cli#run` **skip the `FileUtils.rm_f(Dir.glob(DOWNLOADS/*))` wipe (cli.rb:50)** that used to delete the seed, so `Oddb2xml.skip_download` reuses the seeded `transfer.zip` for ZurRose (no pillbox fetch) while `download_as` still fetches **every other source fresh** (Swissmedic, Refdata, BAG, FHIR NDJSON, Weleda/WALA, and firstbase — proven: `--skip-download` only reuses files already present in `downloads/`, it does not suppress downloading absent ones). Seeding happens **per attempt** inside `run_with_retry`, so a retry restarts from a clean cache (clears partials) while keeping the seed. **Consequence for firstbase:** it is now downloaded **once** by the first build and reused by the `45/50/55`/artikelstamm increments via `--skip-download` — and it is **no longer pre-seeded into `downloads/`** from `$FIRSTBASE_CACHE` (under `--skip-download` a *present* `firstbase.csv` would be reused verbatim and never refreshed, freezing NONPHARMA). GS1's `id.gs1.ch` route currently returns a real 151 MB CSV, so firstbase fetches fresh; `$FIRSTBASE_CACHE` is still refreshed from the fresh file after the build for reference/recovery. The 3.0.29 gem-side `firstbase_csv?`/keep-existing logic is retained but only engages on the non-`--skip-download` path.
72
74
  - **`generate_index_html.sh DOCROOT [FIRSTBASE_CSV]`** — single source of truth for the landing page. Writes `index.html` + a self-contained `logo.svg` **atomically** (temp + `mv`, so either owner — root from setup, `zdavatz` from cron — can refresh it). Computes live counts: PHARMA = `<SMNO>` count in `default/oddb_article.xml`, NONPHARMA = firstbase CSV rows − 1, total ART = `<ART ` count. Also runs **`visitor_stats.py`** and embeds its graph. Re-run standalone any time (it only reads already-built files); a separate cron line refreshes it **hourly** (`5 * * * * zdavatz`) so counts + graph stay current between nightly builds.
73
75
  - **`visitor_stats.py LOG_GLOB CACHE_DIR [DAYS]`** — emits the visitors/sessions/region graph as an inline-SVG HTML **fragment** (last `DAYS`, default 14): Besucher = distinct IPs/day, Sitzungen = 30-min-inactivity sessions per `(IP, User-Agent)`, plus a top-6 country breakdown by IP. Bots are filtered by User-Agent. Region lookup is **fully self-contained** — pure Python stdlib + the free **DB-IP country-lite CSV** (CC-BY, no licence key) cached in the build `downloads/` dir and refreshed monthly; **no apt package, no gem, no system GeoIP DB**. Prints nothing (page degrades to omitting the section) when the Apache log is unreadable or empty. Reading `/var/log/apache2` requires the cron user to be in the **`adm`** group (`sudo usermod -aG adm zdavatz`).
74
76
  - **`swissmedic_watch.sh`** — outage/block auto-recovery (cron: `*/30 * * * * zdavatz`). Since the Swissmedic platform migration (~2026-06-23, now a Swisscom-operated gateway), `www.swissmedic.ch` intermittently resets this host's automated connections **after the TLS handshake** (TCP RST), which aborts `run_oddb2xml.sh` under `set -e` and leaves the feeds stale (the block is host/IP- and client-fingerprint-sensitive: a real browser works, `curl`/`wget`/Ruby get reset, while other admin.ch hosts answer fine — so it is a WAF/bot rule, not an outage). The watcher polls Swissmedic with **oddb2xml's own client** (a Ruby `open-uri` canary on `listen_neu.html`); while blocked it is a silent no-op, and the moment it gets HTTP 200 it launches **one** build and emails. It fires **at most once per day** (stamp in `$STATE_DIR`, default `<OUT_DIR>-watch`, kept **outside** the wiped `$BUILD_DIR`), and skips when a build is already running or today's `default/oddb_article.xml` is already fresh. It exports `RBENV_VERSION=3.4.5` + the rbenv-shims PATH to match the nightly cron (the repo `.ruby-version` pins an uninstalled Ruby).
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oddb2xml (3.0.29)
4
+ oddb2xml (3.0.30)
5
5
  csv
6
6
  htmlentities
7
7
  httpi
data/History.txt CHANGED
@@ -1,3 +1,9 @@
1
+ === 3.0.30 / 11.07.2026
2
+ * New (-r/--rogger): prefer the German article names from the "Rogger Mediliste" (the name-conflict corrections Frau Rogger of Vitabyte/Zur Rose reports, task #OX-5985-1594). RoggerNames.load fetches the CSV export (GTIN,Mediname) of the shared Google Sheet directly at runtime -- sheet edits reach the feeds without any release step -- with a bundled fallback under data/ (a non-CSV response such as a Google sign-in page is rejected and falls back too). Builder#apply_rogger_name_overrides! replaces desc_de for every listed GTIN as the last step of the Refdata description cleanups, so the list wins over the issue-#112 fixes. German-only; FR/IT descriptions and all feeds without -r are unchanged. Initial list: 56 packs covering 13 conflict cases (METOJECT, ELTROXIN LF, CETIRIZIN/GABAPENTIN/ESOMEPRAZOL/EZETIMIB Spirig HC, ATOVAQUON PLUS, MOUNJARO KwikPen, MELATONIN, MONTELUKAST Mepha, FAMPYRA, RINVOQ).
3
+
4
+ === 3.0.29 / 07.07.2026
5
+ * Bugfix (-b/--firstbase): keep the last good firstbase.csv when the GS1 GetFirstbaseHealthcare export answers 403 - Forbidden. FirstbaseDownloader only overwrites the cached CSV when the response is a real non-empty CSV (rejects HTML error pages and open-uri exceptions), so a seeded/last-good file survives a GS1 outage instead of being truncated to zero -- previously every NONPHARMA article vanished from the -b feed. See scripts/run_oddb2xml.sh for the server-side last-good cache.
6
+
1
7
  === 3.0.28 / 30.06.2026
2
8
  * New (--artikelstamm-v5): optionally emit the legacy Elexis Artikelstamm Version 5 file in addition to the v6 file. --artikelstamm now always produces the v6 output (artikelstamm_DDMMYYYY_v6.xml/.csv, with the per-ITEM <ARTSL> BAG indication codes since 3.0.26); passing --artikelstamm-v5 implies --artikelstamm and additionally writes artikelstamm_DDMMYYYY_v5.xml/.csv in the older v5 format (namespace http://elexis.ch/Elexis_Artikelstamm_v5, no <ARTSL> block), validated against the bundled Elexis_Artikelstamm_v5.xsd. The shared download/prepare phase runs once, so both files come from a single build. For consumers that have not yet migrated to v6.
3
9
 
data/README.md CHANGED
@@ -81,6 +81,9 @@ see `--help`.
81
81
  Forces -f dat -p zurrose.
82
82
  -o, --fi Optional fachinfo output.
83
83
  -p, --price Price source (transfer.dat) from ZurRose
84
+ -r, --rogger Prefer the German article names from the Rogger list
85
+ (Vitabyte/Zur Rose name-conflict corrections, fetched
86
+ directly from the shared 'Rogger Mediliste' Google Sheet)
84
87
  -t, --tag-suffix=<s> XML tag suffix S. Default is none. [A-z0-9]
85
88
  If S is given, it is also used as prefix of filename.
86
89
  -x, --context=<s> {product|address}. product is default. (Default: product)
@@ -104,6 +107,7 @@ $ oddb2xml -f dat # => oddb.dat
104
107
  $ oddb2xml -f dat -a nonpharma # => oddb_with_migel.dat
105
108
  $ oddb2xml -e # => oddb_article.xml (FHIR source by default since June 2026)
106
109
  $ oddb2xml -e --no-fhir # => oddb_article.xml using the old BAG-XML Spezialitätenliste
110
+ $ oddb2xml -e -r # => oddb_article.xml with the Rogger-list article names
107
111
  ```
108
112
 
109
113
  output.
@@ -43,7 +43,7 @@ module Oddb2xml
43
43
  attr_accessor :subject, :refdata, :items, :flags, :lppvs,
44
44
  :actions, :migel, :orphan,
45
45
  :infos, :packs, :infos_zur_rose, :firstbase,
46
- :weleda_sl,
46
+ :weleda_sl, :rogger_names,
47
47
  :ean14, :tag_suffix,
48
48
  :companies, :people,
49
49
  :xsd
@@ -59,6 +59,7 @@ module Oddb2xml
59
59
  @migel = {}
60
60
  @infos_zur_rose ||= {}
61
61
  @weleda_sl ||= {}
62
+ @rogger_names ||= {}
62
63
  @firstbase ||= {}
63
64
  @actions = []
64
65
  @orphan = []
@@ -138,6 +139,24 @@ module Oddb2xml
138
139
  counts.each do |rule, n|
139
140
  Oddb2xml.log("Refdata cleanup: fixed #{labels[rule]} in #{n} description(s)") if n > 0
140
141
  end
142
+ apply_rogger_name_overrides!
143
+ end
144
+
145
+ # -r/--rogger: replace the German description with the preferred name from
146
+ # the Rogger list (see Oddb2xml::RoggerNames) for every listed GTIN. Runs
147
+ # after the issue-#112 cleanups so the list always wins. The list is
148
+ # German-only, FR/IT descriptions are left untouched.
149
+ def apply_rogger_name_overrides!
150
+ return if @rogger_names.nil? || @rogger_names.empty?
151
+ count = 0
152
+ @refdata.each_value do |item|
153
+ next unless item.is_a?(Hash)
154
+ name = @rogger_names[item[:ean13].to_s.rjust(13, "0")]
155
+ next if name.nil? || name.empty? || item[:desc_de] == name
156
+ item[:desc_de] = name
157
+ count += 1
158
+ end
159
+ Oddb2xml.log("RoggerNames: overrode #{count} German description(s)") if count > 0
141
160
  end
142
161
 
143
162
  private_class_method
data/lib/oddb2xml/cli.rb CHANGED
@@ -5,6 +5,7 @@ require "oddb2xml/compressor"
5
5
  require "oddb2xml/options"
6
6
  require "oddb2xml/proxy_check"
7
7
  require "oddb2xml/weleda_sl"
8
+ require "oddb2xml/rogger_names"
8
9
  require "oddb2xml/util"
9
10
  require "rubyXL"
10
11
  require "date" # for today
@@ -168,6 +169,10 @@ module Oddb2xml
168
169
  else
169
170
  {}
170
171
  end
172
+ # -r/--rogger: preferred German article names from the Rogger list
173
+ # (Vitabyte/Zur Rose name-conflict corrections). Applied at the end
174
+ # of Builder#apply_refdata_description_cleanups!.
175
+ builder.rogger_names = @options[:rogger] ? Oddb2xml::RoggerNames.load(@options) : {}
171
176
  end
172
177
  builder.tag_suffix = @options[:tag_suffix]
173
178
  end
@@ -203,6 +203,20 @@ module Oddb2xml
203
203
  end
204
204
  end
205
205
 
206
+ # "Rogger Mediliste": GTIN -> preferred German article name (Vitabyte/Zur
207
+ # Rose name-conflict corrections). Fetched directly as the CSV export of the
208
+ # shared Google Sheet (the source of truth Frau Rogger maintains), so edits
209
+ # there reach the feeds without any release step. The sheet must be shared
210
+ # as "anyone with the link can view". See Oddb2xml::RoggerNames.
211
+ class RoggerDownloader < Downloader
212
+ include DownloadMethod
213
+ ROGGER_SHEET_ID = "1NXJZ8KYzVsX0OQU767tl_AwCyvFVieHWnTWXqhflwdc"
214
+ def download
215
+ @url ||= "https://docs.google.com/spreadsheets/d/#{ROGGER_SHEET_ID}/export?format=csv&gid=0"
216
+ download_as("rogger_liste.csv", "w+")
217
+ end
218
+ end
219
+
206
220
  class ZurroseDownloader < Downloader
207
221
  include DownloadMethod
208
222
  def download
@@ -36,6 +36,9 @@ module Oddb2xml
36
36
  Forces -f dat -p zurrose.", type: :int, default: nil, short: "I"
37
37
  opt :fi, "Optional fachinfo output.", short: "o"
38
38
  opt :price, "Price source (transfer.dat) from ZurRose", default: nil
39
+ opt :rogger, "Prefer the German article names from the Rogger list
40
+ (Vitabyte/Zur Rose name-conflict corrections, fetched
41
+ directly from the shared 'Rogger Mediliste' Google Sheet)", short: "r", default: false
39
42
  opt :tag_suffix, "XML tag suffix S. Default is none. [A-z0-9]
40
43
  If S is given, it is also used as prefix of filename.", type: :string, short: "t"
41
44
  opt :context, "{product|address}. product is default.", default: "product", type: :string, short: "x"
@@ -1,3 +1,3 @@
1
1
  module Oddb2xml
2
- VERSION = "3.0.29"
2
+ VERSION = "3.0.30"
3
3
  end
@@ -81,62 +81,70 @@ rm -rf "$BUILD_DIR"
81
81
  mkdir -p "$BUILD_DIR"
82
82
  cd "$BUILD_DIR"
83
83
 
84
- # 3. Seed the ZurRose transfer.zip from the local get_transfer mirror.
85
- # get_transfer.sh (crontab 00:30) downloads transfer.dat straight from
86
- # zurrose.ch on THIS host and uploads the zip to pillbox.oddb.org so the
87
- # pillbox HTTP fetch is a needless detour back to our own file. Placing the
88
- # zip in downloads/ makes oddb2xml's skip_download reuse it and the build no
89
- # longer depends on pillbox being up (2026-07-02: pillbox refused connections
90
- # during the 01:00 run and all three retries died, killing the whole nightly
91
- # build). If the seed file is missing, oddb2xml falls back to the normal
92
- # pillbox download as before.
84
+ # 3. ZurRose transfer.zip source. get_transfer.sh (crontab 00:30) downloads
85
+ # transfer.dat straight from zurrose.ch on THIS host and mirrors the zip locally
86
+ # (it also uploads it to http://pillbox.oddb.org/TRANSFER.ZIP). The build seeds
87
+ # this local copy into a fresh downloads/ (see seed_downloads below) so the first
88
+ # build reuses it via --skip-download instead of fetching pillbox which refused
89
+ # connections around 01:00 every night from 2026-07-04 on and aborted the whole
90
+ # run at the ZurRose step (downloader.rb:221, transfer.dat ENOENT). Everything
91
+ # else is still fetched fresh: --skip-download only reuses files already present
92
+ # in downloads/ and downloads the rest.
93
93
  GET_TRANSFER_ZIP="${GET_TRANSFER_ZIP:-/home/zdavatz/software/get_transfer/TRANSFER.ZIP}"
94
- if [[ -s "$GET_TRANSFER_ZIP" ]]; then
95
- mkdir -p "$BUILD_DIR/downloads"
96
- cp -p "$GET_TRANSFER_ZIP" "$BUILD_DIR/downloads/transfer.zip"
97
- log "Seeded ZurRose transfer.zip from $GET_TRANSFER_ZIP ($(date -r "$GET_TRANSFER_ZIP" '+%Y-%m-%d %H:%M'), no pillbox fetch needed)"
98
- else
99
- log "WARNING: $GET_TRANSFER_ZIP missing - falling back to pillbox.oddb.org download"
100
- fi
101
-
102
- # 3b. Firstbase (GS1 NONPHARMA) fallback. The GS1 GetFirstbaseHealthcare
103
- # endpoint has been answering "403 - Forbidden", which blanked firstbase.csv and
104
- # dropped every NONPHARMA article from the -b feed. Keep the last successful
105
- # firstbase.csv in a persistent cache OUTSIDE $BUILD_DIR (it survives the nightly
106
- # `rm -rf`) and seed it into downloads/ so the gem's FirstbaseDownloader falls
107
- # back to yesterday's file when today's download fails. A recovered GS1 still
108
- # refreshes the data: the first (downloading) build always retries the live
109
- # fetch and only keeps the seed when that fetch yields no CSV.
94
+ [[ -s "$GET_TRANSFER_ZIP" ]] || log "WARNING: $GET_TRANSFER_ZIP missing - ZurRose will fall back to pillbox.oddb.org"
95
+
96
+ # 3b. Firstbase (GS1 NONPHARMA) last-good cache. Because the first build now runs
97
+ # with --skip-download (to keep the ZurRose seed, see above), firstbase.csv must
98
+ # NOT be pre-seeded into downloads/: under --skip-download a present firstbase.csv
99
+ # would be reused verbatim and never refreshed from GS1. Instead firstbase is
100
+ # fetched fresh every run (GS1's id.gs1.ch route works) and the fresh file is
101
+ # archived to this persistent cache after the build for reference / recovery.
110
102
  FIRSTBASE_CACHE="${FIRSTBASE_CACHE:-${OUT_DIR%/}-state/firstbase.csv}"
111
- if [[ -s "$FIRSTBASE_CACHE" ]]; then
103
+
104
+ # seed_downloads — reset downloads/ so it contains only the ZurRose transfer.zip
105
+ # seed. A following --skip-download build reuses that zip (no pillbox fetch) and
106
+ # downloads every other source fresh. Called before each attempt of the first
107
+ # build, so a retry restarts from a clean cache (clearing any partial download)
108
+ # while preserving the seed.
109
+ seed_downloads() {
110
+ rm -rf "$BUILD_DIR/downloads"
112
111
  mkdir -p "$BUILD_DIR/downloads"
113
- cp -p "$FIRSTBASE_CACHE" "$BUILD_DIR/downloads/firstbase.csv"
114
- log "Seeded firstbase.csv from last-good cache $FIRSTBASE_CACHE ($(($(wc -l < "$FIRSTBASE_CACHE") - 1)) rows, $(date -r "$FIRSTBASE_CACHE" '+%Y-%m-%d %H:%M'))"
115
- else
116
- log "No firstbase last-good cache at $FIRSTBASE_CACHE yet - relying on live GS1 download"
117
- fi
112
+ if [[ -s "$GET_TRANSFER_ZIP" ]]; then
113
+ cp -p "$GET_TRANSFER_ZIP" "$BUILD_DIR/downloads/transfer.zip"
114
+ log "Seeded ZurRose transfer.zip from $GET_TRANSFER_ZIP ($(date -r "$GET_TRANSFER_ZIP" '+%Y-%m-%d %H:%M'))"
115
+ fi
116
+ }
117
+
118
+ # first_build_attempt — seed downloads/, then run the first (downloading) build.
119
+ # Wrapped in run_with_retry so each retry re-seeds and re-downloads cleanly.
120
+ first_build_attempt() {
121
+ seed_downloads
122
+ "$ODDB2XML_BIN" --skip-download -b "$@" -c zip
123
+ }
118
124
 
119
125
  first=1
120
126
 
121
127
  # build_one <increment-percent|""> <destination-subdir>
122
128
  build_one() {
123
129
  local inc="$1" name="$2" dest="$OUT_DIR/$2"
124
- local inc_opt=() dl_opt=()
130
+ local inc_opt=()
125
131
  [[ -n "$inc" ]] && inc_opt=(-I "$inc")
126
132
 
133
+ log "Building increment '${inc:-none}' -> $dest"
134
+ rm -f oddb*.zip
127
135
  if [[ $first -eq 1 ]]; then
128
- first=0 # first build downloads the sources
136
+ first=0
137
+ # First build: seed the ZurRose zip into a clean downloads/, then fetch every
138
+ # other source fresh. Runs with --skip-download so cli.rb does not wipe
139
+ # downloads/ and the seeded transfer.zip survives (no pillbox fetch).
140
+ run_with_retry "oddb2xml build '${inc:-none}'" -- first_build_attempt "${inc_opt[@]}"
129
141
  else
130
- dl_opt=(--skip-download) # the rest re-use the cached downloads/
142
+ # Subsequent increments re-use the fully-populated downloads/ cache (firstbase
143
+ # and everything else were downloaded once by the first build).
144
+ run_with_retry "oddb2xml build '${inc:-none}'" -- \
145
+ "$ODDB2XML_BIN" --skip-download -b "${inc_opt[@]}" -c zip
131
146
  fi
132
147
 
133
- log "Building increment '${inc:-none}' -> $dest"
134
- rm -f oddb*.zip
135
- # On a retry the first build re-downloads from scratch (dl_opt empty), which
136
- # also clears any partial download left by the failed attempt.
137
- run_with_retry "oddb2xml build '${inc:-none}'" -- \
138
- "$ODDB2XML_BIN" "${dl_opt[@]}" -b "${inc_opt[@]}" -c zip
139
-
140
148
  shopt -s nullglob
141
149
  local zips=(oddb*.zip)
142
150
  shopt -u nullglob
data/spec/options_spec.rb CHANGED
@@ -5,6 +5,8 @@ Oddb2xml::DEFAULT_OPTS = {
5
5
  fi: false,
6
6
  address: false,
7
7
  artikelstamm: false,
8
+ artikelstamm_v5: false,
9
+ rogger: false,
8
10
  nonpharma: false,
9
11
  extended: false,
10
12
  compress_ext: nil,
@@ -55,6 +57,18 @@ describe Oddb2xml::Options do
55
57
  specify { expect(test_opts).to eq expected }
56
58
  end
57
59
 
60
+ context "when -r is given" do
61
+ test_opts = Oddb2xml::Options.parse("-r")
62
+ expected = Oddb2xml::DEFAULT_OPTS.clone
63
+ expected[:rogger] = true
64
+ specify { expect(test_opts).to eq expected }
65
+ end
66
+
67
+ context "when --rogger is given" do
68
+ test_opts = Oddb2xml::Options.parse("--rogger")
69
+ specify { expect(test_opts[:rogger]).to eq true }
70
+ end
71
+
58
72
  context "when -a is given" do
59
73
  test_opts = Oddb2xml::Options.parse("-a")
60
74
  expected = Oddb2xml::DEFAULT_OPTS.clone
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oddb2xml
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.29
4
+ version: 3.0.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yasuhiro Asaka, Zeno R.R. Davatz, Niklaus Giger
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-07-11 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rubyzip
@@ -590,6 +591,7 @@ homepage: https://github.com/zdavatz/oddb2xml
590
591
  licenses:
591
592
  - GPL-3.0-only
592
593
  metadata: {}
594
+ post_install_message:
593
595
  rdoc_options: []
594
596
  require_paths:
595
597
  - lib
@@ -604,7 +606,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
604
606
  - !ruby/object:Gem::Version
605
607
  version: '0'
606
608
  requirements: []
607
- rubygems_version: 3.6.9
609
+ rubygems_version: 3.5.22
610
+ signing_key:
608
611
  specification_version: 4
609
612
  summary: oddb2xml creates xml files.
610
613
  test_files: