oddb2xml 3.0.29 → 3.0.31
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 +4 -4
- data/CLAUDE.md +4 -32
- data/Gemfile.lock +4 -4
- data/History.txt +9 -0
- data/README.md +4 -0
- data/data/rogger_liste.csv +57 -0
- data/lib/oddb2xml/builder.rb +20 -1
- data/lib/oddb2xml/cli.rb +5 -0
- data/lib/oddb2xml/downloader.rb +14 -0
- data/lib/oddb2xml/fhir_support.rb +8 -5
- data/lib/oddb2xml/options.rb +3 -0
- data/lib/oddb2xml/rogger_names.rb +86 -0
- data/lib/oddb2xml/version.rb +1 -1
- data/scripts/run_oddb2xml.sh +49 -41
- data/spec/fhir_spec.rb +47 -0
- data/spec/options_spec.rb +14 -0
- data/spec/rogger_names_spec.rb +88 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8ae3ea587547caf78cba9d778e08e6474535800d111cf484f981f5eb86cc7c2
|
|
4
|
+
data.tar.gz: cf33dfe5d3b549fa1c2fc1542cc0f7754e369a8d50c757705c869ba119b245b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b49924c39835eac2d61782b95edbd7564f704ca59fdcf0404f04f8b58f4bf09390d41e2676a974a4803a10ce7c8751bbc96177f6e94ff94cda49277b2e946426
|
|
7
|
+
data.tar.gz: 248e0058044934b2986855523747640fa8c1cc4652a039d7b0427fbcf6e29358fa5fec55a42043042d49fc7368ccc2c7cdbc20ed4224259e586cc023db5fd5fd
|
data/CLAUDE.md
CHANGED
|
@@ -6,31 +6,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
6
6
|
|
|
7
7
|
oddb2xml is a Ruby gem that downloads Swiss pharmaceutical data from 10+ sources (Swissmedic, BAG, Refdata, ZurRose, EPha, etc.), parses multiple formats (XML, XLSX, CSV, SOAP, fixed-width DAT), merges/deduplicates them, and generates standardized XML/DAT output files for healthcare systems. It also supports the Elexis EHR Artikelstamm format.
|
|
8
8
|
|
|
9
|
-
## Common Commands
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
# Install dependencies
|
|
13
|
-
bundle install
|
|
14
|
-
|
|
15
|
-
# Run full test suite
|
|
16
|
-
bundle exec rake spec
|
|
17
|
-
|
|
18
|
-
# Run a single test file
|
|
19
|
-
bundle exec rspec spec/builder_spec.rb
|
|
20
|
-
|
|
21
|
-
# Run a single test by line number
|
|
22
|
-
bundle exec rspec spec/builder_spec.rb:42
|
|
23
|
-
|
|
24
|
-
# Lint with StandardRB
|
|
25
|
-
bundle exec standardrb
|
|
26
|
-
|
|
27
|
-
# Auto-fix lint issues
|
|
28
|
-
bundle exec standardrb --fix
|
|
29
|
-
|
|
30
|
-
# Build the gem
|
|
31
|
-
bundle exec rake build
|
|
32
|
-
```
|
|
33
|
-
|
|
34
9
|
## Architecture
|
|
35
10
|
|
|
36
11
|
The system follows a **download → extract → build → compress** pipeline:
|
|
@@ -47,7 +22,7 @@ The system follows a **download → extract → build → compress** pipeline:
|
|
|
47
22
|
|
|
48
23
|
6. **Compressor** (`lib/oddb2xml/compressor.rb`) — Optional ZIP/TAR.GZ output compression.
|
|
49
24
|
|
|
50
|
-
7. **FHIR support** (`lib/oddb2xml/fhir_support.rb`) — Self-contained module providing `FhirDownloader` and FHIR NDJSON parsing. Activated via `--fhir` (or `--fhir-url=<URL>`). Downloads per-language NDJSON files (`foph-sl-export-latest-{de,fr,it}.ndjson`) from `epl.bag.admin.ch` to populate French and Italian product names/descriptions. Maps legal status codes `756005022007` and `756005022008` to Swissmedic category D. Reads the BAG **Indikationscode** (`XXXXX.NN`) from the explicit `indicationCode` extension on each `RegulatedAuthorization.indication[].extension[regulatedAuthorization-limitation]` (BAG SL FHIR export >= v2.0.5; handled from 3.0.10). The BAG changelog states the limitation code (`ClinicalUseDefinition.id`) and the indication code are **independent** fields, so the older derivation — combining each indication CUD's `.NN` id-suffix with the reimbursement RA's `FOPHDossierNumber` — is kept only as a fallback for feeds lacking the extension. Exposed as `item[:indication_codes]` and per-package `:indication_codes` (each entry a `{code:, cud_id:, text:}` hash, where `cud_id` is the `limitationIndication` CUD reference used to resolve the text). From 3.0.7 onwards, `Builder#build_product` emits one `<INDICATION_CODE code="XXXXX.NN" cud_id="DRUG.NN">limitation text</INDICATION_CODE>` child per indication on every `<PRD>` in `oddb_product.xml`; live feed numbers: 539 products / 1,293 codes / 100 % with non-empty indication text. Mandatory on prescriptions/invoices for SL price-model drugs from 2026-07-01 — see issue [#113](https://github.com/zdavatz/oddb2xml/issues/113). **Limitation texts** (3.0.8 onwards): the `regulatedAuthorization-limitation` extension has no inline `limitationText` in the live BAG feed — it carries a `limitationIndication` reference to a `ClinicalUseDefinition` whose `indication.diseaseSymptomProcedure.concept.text` is the actual text. The parser stores the ref as `cud_ref` on each Limitation, `Bundle#cud_text_by_id` resolves DE, and `merge_language` propagates FR/IT from the per-language NDJSON files via the same CUD id. Coverage on the live feed jumped from 0 / 9'108 to 9'108 / 9'108 (issue [#116](https://github.com/zdavatz/oddb2xml/issues/116)). **Limitation code / LIMNAMEBAG** (3.0.12 onwards): FHIR has no native BAG limitation code (LIMCD), so `create_limitations_for_package` sets `LimitationCode = cud_ref` (the `limitationIndication` CUD id) instead of `""`. Without this, every FHIR limitation shared an empty `:code`; `Builder#build_artikelstamm` groups its `<LIMITATIONS>` section by code, so all of them collapsed into a single `<LIMITATION>` with an empty `<LIMNAMEBAG>` and only one text survived. Using the CUD id as the key makes each distinct limitation emit and be referenced from its `<PRODUCT>`. The downstream `bin/check_artikelstamm` (`semantic_check.rb`) also crashed on the lone-element output because Ox `:hash_no_attrs` collapses a one-child section into a Hash (and an empty one into nil) — `SemanticCheckXML#get_items` now normalises every section to an Array. **v6 Artikelstamm / per-article INDC (3.0.26 onwards):** `--artikelstamm` now emits the **Elexis Artikelstamm v6** format (namespace `http://elexis.ch/Elexis_Artikelstamm_v6`, file `artikelstamm_DDMMYYYY_v6.xml`/`.csv`, validated against the bundled `Elexis_Artikelstamm_v6.xsd`) — replacing v5. The new piece is a per-`<ITEM>` `<ARTSL>` block carrying the BAG Indikationscodes (issue [#113](https://github.com/zdavatz/oddb2xml/issues/113)): `<PM>true</PM>` plus one `<ARTLIM>` per limitation with `<LIMCD>` (= `cud_ref`, the BAG limitation code), `<INDCD>` (the `XXXXX.NN` indication code from the `indicationCode` extension), and `<VDAT>`/`<VTDAT>` (period start/end). To feed it, `create_limitations_for_package` now also carries `IndicationCode` (→ per-package `:indcd`) and `ValidThruDate` (→ `:vtdate`) on each limitation; `Builder#append_artsl`/`elexis_datetime` emit one `<ARTLIM>` per limitation that has a non-empty `:indcd` (so non-price-model items get no `<ARTSL>`). `PM` is always `true` here because the indication code is required only for SL price-model drugs, which is exactly the set of items that reach this block. The bundled `Elexis_Artikelstamm_v6.xsd` is the canonical MEDEVIT schema extended with oddb2xml's historical Italian elements (`DSCRI` on PRODUCT/LIMITATION/ITEM, `DOSAGE_FORMI` on ITEM) so the output still validates. The legacy `--no-fhir` path emits no `<ARTSL>` (no FHIR limitations). **Per-article INDC in the `-e`/`-b` feeds (3.0.27 onwards):** the same indication codes are also emitted per `<ART>` in `oddb_article.xml` via `Builder#append_indication_codes` — one `<INDICATION_CODE code="XXXXX.NN" cud_id="DRUG.NN" limcd="DRUG.NN" vdat=… vtdat=…>limitation text</INDICATION_CODE>` per limitation that carries an `:indcd` (sourced from the article's `pac[:limitations]`). This reuses the flat `<INDICATION_CODE>` element already emitted on `<PRD>` in `oddb_product.xml` (since 3.0.7), enriched with the limitation code and validity dates so it carries the full `<ARTSL>` payload. The bundled `oddb2xml.xsd` now defines `<INDICATION_CODE>` (simpleContent + `code`/`cud_id`/`limcd`/`vdat`/`vtdat` attributes) and references it from both `<ART>` and `<PRD>` — the `<PRD>` reference had been missing since the 3.0.7 addition, so the FHIR product feed only validated once this was added. **Legacy v5 opt-in (3.0.28 onwards):** `--artikelstamm-v5` additionally emits the older Artikelstamm v5 file alongside the v6 one (for consumers not yet migrated). It implies `--artikelstamm`; `Options.parse` sets `@opts[:artikelstamm] = true`. `build_artikelstamm` takes a `version:` keyword (default `6`) that drives the namespace (`Elexis_Artikelstamm_v#{version}`) and CSV/XML filename suffix; `build_artikelstamm_v5` calls it with `version: 5`. The only content difference is that the v5 path skips `append_artsl` (the v5 schema has no `<ARTSL>` element). The CLI `files` map adds an `:artikelstamm_v5` entry (→ `build_artikelstamm_v5` via the `to_xml` subject dispatch) and validates it against the bundled `Elexis_Artikelstamm_v5.xsd`. Both files come from one build — the `@prepared` guard means the shared download/prepare phase runs once.
|
|
25
|
+
7. **FHIR support** (`lib/oddb2xml/fhir_support.rb`) — Self-contained module providing `FhirDownloader` and FHIR NDJSON parsing. Activated via `--fhir` (or `--fhir-url=<URL>`). Downloads per-language NDJSON files (`foph-sl-export-latest-{de,fr,it}.ndjson`) from `epl.bag.admin.ch` to populate French and Italian product names/descriptions. Maps legal status codes `756005022007` and `756005022008` to Swissmedic category D. Reads the BAG **Indikationscode** (`XXXXX.NN`) from the explicit `indicationCode` extension on each `RegulatedAuthorization.indication[].extension[regulatedAuthorization-limitation]` (BAG SL FHIR export >= v2.0.5; handled from 3.0.10). The BAG changelog states the limitation code (`ClinicalUseDefinition.id`) and the indication code are **independent** fields, so the older derivation — combining each indication CUD's `.NN` id-suffix with the reimbursement RA's `FOPHDossierNumber` — is kept only as a fallback for feeds lacking the extension. Exposed as `item[:indication_codes]` and per-package `:indication_codes` (each entry a `{code:, cud_id:, text:}` hash, where `cud_id` is the `limitationIndication` CUD reference used to resolve the text). From 3.0.7 onwards, `Builder#build_product` emits one `<INDICATION_CODE code="XXXXX.NN" cud_id="DRUG.NN">limitation text</INDICATION_CODE>` child per indication on every `<PRD>` in `oddb_product.xml`; live feed numbers: 539 products / 1,293 codes / 100 % with non-empty indication text. Mandatory on prescriptions/invoices for SL price-model drugs from 2026-07-01 — see issue [#113](https://github.com/zdavatz/oddb2xml/issues/113). **Limitation texts** (3.0.8 onwards): the `regulatedAuthorization-limitation` extension has no inline `limitationText` in the live BAG feed — it carries a `limitationIndication` reference to a `ClinicalUseDefinition` whose `indication.diseaseSymptomProcedure.concept.text` is the actual text. The parser stores the ref as `cud_ref` on each Limitation, `Bundle#cud_text_by_id` resolves DE, and `merge_language` propagates FR/IT from the per-language NDJSON files via the same CUD id. Coverage on the live feed jumped from 0 / 9'108 to 9'108 / 9'108 (issue [#116](https://github.com/zdavatz/oddb2xml/issues/116)). **Limitation code / LIMNAMEBAG** (3.0.12 onwards): FHIR has no native BAG limitation code (LIMCD), so `create_limitations_for_package` sets `LimitationCode = cud_ref` (the `limitationIndication` CUD id) instead of `""`. Without this, every FHIR limitation shared an empty `:code`; `Builder#build_artikelstamm` groups its `<LIMITATIONS>` section by code, so all of them collapsed into a single `<LIMITATION>` with an empty `<LIMNAMEBAG>` and only one text survived. Using the CUD id as the key makes each distinct limitation emit and be referenced from its `<PRODUCT>`. The downstream `bin/check_artikelstamm` (`semantic_check.rb`) also crashed on the lone-element output because Ox `:hash_no_attrs` collapses a one-child section into a Hash (and an empty one into nil) — `SemanticCheckXML#get_items` now normalises every section to an Array. **v6 Artikelstamm / per-article INDC (3.0.26 onwards):** `--artikelstamm` now emits the **Elexis Artikelstamm v6** format (namespace `http://elexis.ch/Elexis_Artikelstamm_v6`, file `artikelstamm_DDMMYYYY_v6.xml`/`.csv`, validated against the bundled `Elexis_Artikelstamm_v6.xsd`) — replacing v5. The new piece is a per-`<ITEM>` `<ARTSL>` block carrying the BAG Indikationscodes (issue [#113](https://github.com/zdavatz/oddb2xml/issues/113)): `<PM>true</PM>` plus one `<ARTLIM>` per limitation with `<LIMCD>` (= `cud_ref`, the BAG limitation code), `<INDCD>` (the `XXXXX.NN` indication code from the `indicationCode` extension), and `<VDAT>`/`<VTDAT>` (period start/end). To feed it, `create_limitations_for_package` now also carries `IndicationCode` (→ per-package `:indcd`) and `ValidThruDate` (→ `:vtdate`) on each limitation; `Builder#append_artsl`/`elexis_datetime` emit one `<ARTLIM>` per limitation that has a non-empty `:indcd` (so non-price-model items get no `<ARTSL>`). `PM` is always `true` here because the indication code is required only for SL price-model drugs, which is exactly the set of items that reach this block. The bundled `Elexis_Artikelstamm_v6.xsd` is the canonical MEDEVIT schema extended with oddb2xml's historical Italian elements (`DSCRI` on PRODUCT/LIMITATION/ITEM, `DOSAGE_FORMI` on ITEM) so the output still validates. The legacy `--no-fhir` path emits no `<ARTSL>` (no FHIR limitations). **Per-article INDC in the `-e`/`-b` feeds (3.0.27 onwards):** the same indication codes are also emitted per `<ART>` in `oddb_article.xml` via `Builder#append_indication_codes` — one `<INDICATION_CODE code="XXXXX.NN" cud_id="DRUG.NN" limcd="DRUG.NN" vdat=… vtdat=…>limitation text</INDICATION_CODE>` per limitation that carries an `:indcd` (sourced from the article's `pac[:limitations]`). This reuses the flat `<INDICATION_CODE>` element already emitted on `<PRD>` in `oddb_product.xml` (since 3.0.7), enriched with the limitation code and validity dates so it carries the full `<ARTSL>` payload. The bundled `oddb2xml.xsd` now defines `<INDICATION_CODE>` (simpleContent + `code`/`cud_id`/`limcd`/`vdat`/`vtdat` attributes) and references it from both `<ART>` and `<PRD>` — the `<PRD>` reference had been missing since the 3.0.7 addition, so the FHIR product feed only validated once this was added. **Legacy v5 opt-in (3.0.28 onwards):** `--artikelstamm-v5` additionally emits the older Artikelstamm v5 file alongside the v6 one (for consumers not yet migrated). It implies `--artikelstamm`; `Options.parse` sets `@opts[:artikelstamm] = true`. `build_artikelstamm` takes a `version:` keyword (default `6`) that drives the namespace (`Elexis_Artikelstamm_v#{version}`) and CSV/XML filename suffix; `build_artikelstamm_v5` calls it with `version: 5`. The only content difference is that the v5 path skips `append_artsl` (the v5 schema has no `<ARTSL>` element). The CLI `files` map adds an `:artikelstamm_v5` entry (→ `build_artikelstamm_v5` via the `to_xml` subject dispatch) and validates it against the bundled `Elexis_Artikelstamm_v5.xsd`. Both files come from one build — the `@prepared` guard means the shared download/prepare phase runs once. **Selbstbehalt / costShare (3.0.31 onwards):** the `reimbursementSL` extension's `costShare` valueInteger is the BAG Selbstbehalt percentage; it maps to the legacy `FlagSB` semantics of the old BAG `Preparations.xml` — `40` → `item[:deductible] = "Y"` (raised Selbstbehalt: `<SLOPLUS>1</SLOPLUS>` in `oddb_article.xml`, `<DEDUCTIBLE>40</DEDUCTIBLE>` in the Artikelstamm), `10` → `"N"` (ordinary Selbstbehalt: `SLOPLUS` 2 / `DEDUCTIBLE` 10), `20` → `deductible20 = "Y"` (transitional rate, issue #81). Until 3.0.31 the mapping was inverted (`10` → `"Y"`), so **every** SL article claimed a 40 % Selbstbehalt. On the live feed only 385 of 10'388 packages (222 preparations) carry `costShare` 40; the value never mixes within one preparation, so keeping the flag at sequence level is safe.
|
|
51
26
|
|
|
52
27
|
8. **Refdata cleanup** (`lib/oddb2xml/refdata_cleanup.rb`) — Compensates for known data-quality issues in upstream Refdata.Articles.xml before they reach the output. Each fix is guarded by a Swissmedic-side heuristic (e.g. comma in `substance_swissmedic` to distinguish mono products from real combinations). Currently fixes (a) the doubled-dose template bug (`X mg / X mg / Stk`, `fix_double_dose`, guarded by `single_substance?`); (b) the spelled-out German galenic form `Retardtabletten` → house-style abbreviation `Ret Tabl` (`normalize_galenic_form` / `GALENIC_NORMALISATIONS`, issue #112 case #13, e.g. RINVOQ — a narrow word-boundary substitution that leaves legitimate brand suffixes like `TRAMAL retard` and Mepha's `Lactab` untouched); and (c) dose info Refdata dropped from `<FullName>`, sourced from the Swissmedic composition string `pack[:composition_swissmedic]` — `fix_missing_combo_dose` (#6, appends a combination's 2nd component strength), `fix_missing_dose` (#4, inserts a mono product's missing strength before the pack count), `fix_missing_volume` (#7, appends an injectable's per-pen volume); and (d) 50-char-truncation repairs — `fix_truncated_metoject` (#1, rebuilds METOJECT Autoinjektor names from the intact `<brand> Autoinjektor <dose>/<vol>` prefix + Swissmedic `size`, localised DE/FR/IT) and `fix_truncated_volume_unit` (#3, restores the cut `ml` of the VERACTIV Vitamin D3 drops). The (c) and (d) fixes are scoped to explicit IKSNR allow-lists (`COMBO_DOSE_IKSNR`/`MISSING_DOSE_IKSNR`/`MISSING_VOLUME_IKSNR`/`METOJECT_IKSNR`/`VERACTIV_VITD3_IKSNR`): a dry run proved a blanket heuristic mis-fires on hundreds of legitimate names (sodium counter-ion doses, strength-less phyto/powder products, concentration names like `CIMZIA 200 mg/ml`), so only catalogued registrations are touched — add an IKSNR to grow coverage. Called from `Builder#apply_refdata_description_cleanups!` at the start of `prepare_articles`. See GitHub issue #112 for the catalogue.
|
|
53
28
|
|
|
@@ -55,6 +30,8 @@ The system follows a **download → extract → build → compress** pipeline:
|
|
|
55
30
|
|
|
56
31
|
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
32
|
|
|
33
|
+
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).
|
|
34
|
+
|
|
58
35
|
### Key data identifiers
|
|
59
36
|
- **GTIN/EAN13**: Primary article identifier (13-digit barcode)
|
|
60
37
|
- **Pharmacode**: Swiss pharmacy code
|
|
@@ -68,7 +45,7 @@ YAML files in `data/` provide manual overrides and mappings: `article_overrides.
|
|
|
68
45
|
|
|
69
46
|
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
47
|
|
|
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`).
|
|
48
|
+
- **`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
49
|
- **`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
50
|
- **`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
51
|
- **`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).
|
|
@@ -83,8 +60,3 @@ Only the scripts are git-tracked; the generated `index.html`/`logo.svg` and the
|
|
|
83
60
|
- Test fixtures: `spec/data/` (sample files), `spec/fixtures/vcr_cassettes/` (recorded HTTP responses)
|
|
84
61
|
- `spec/spec_helper.rb` defines test constants (GTINs) and configures VCR to avoid real HTTP calls during tests
|
|
85
62
|
- CI runs on Ruby 3.0, 3.1, 3.2
|
|
86
|
-
|
|
87
|
-
## Ruby Version
|
|
88
|
-
|
|
89
|
-
- Minimum: Ruby >= 2.5.0 (gemspec)
|
|
90
|
-
- Current development: Ruby 3.3.6 (`.ruby-version`)
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
oddb2xml (3.0.
|
|
4
|
+
oddb2xml (3.0.31)
|
|
5
5
|
csv
|
|
6
6
|
htmlentities
|
|
7
7
|
httpi
|
|
@@ -89,10 +89,10 @@ GEM
|
|
|
89
89
|
net-http-persistent (4.0.8)
|
|
90
90
|
connection_pool (>= 2.2.4, < 4)
|
|
91
91
|
nkf (0.2.0)
|
|
92
|
-
nokogiri (1.19.
|
|
92
|
+
nokogiri (1.19.4)
|
|
93
93
|
mini_portile2 (~> 2.8.2)
|
|
94
94
|
racc (~> 1.4)
|
|
95
|
-
nokogiri (1.19.
|
|
95
|
+
nokogiri (1.19.4-arm64-darwin)
|
|
96
96
|
racc (~> 1.4)
|
|
97
97
|
nori (2.7.1)
|
|
98
98
|
bigdecimal
|
|
@@ -210,7 +210,7 @@ GEM
|
|
|
210
210
|
webrobots (0.1.2)
|
|
211
211
|
xml-simple (1.1.9)
|
|
212
212
|
rexml
|
|
213
|
-
yard (0.9.
|
|
213
|
+
yard (0.9.44)
|
|
214
214
|
|
|
215
215
|
PLATFORMS
|
|
216
216
|
arm64-darwin-25
|
data/History.txt
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
=== 3.0.31 / 14.07.2026
|
|
2
|
+
* Bugfix (SLOPLUS / DEDUCTIBLE): every SL article claimed the raised 40% Selbstbehalt. The FHIR extractor mapped the BAG costShare percentage the wrong way round -- costShare 10 (the ordinary 10% Selbstbehalt, carried by ~97% of all packages) was turned into deductible "Y", which Builder#build_article emits as <SLOPLUS>1</SLOPLUS> (40%) and build_artikelstamm as <DEDUCTIBLE>40</DEDUCTIBLE>. costShare now maps 40 => "Y" (SLOPLUS 1 / DEDUCTIBLE 40), 10 => "N" (SLOPLUS 2 / DEDUCTIBLE 10), 20 => deductible20 "Y" (transitional rate, issue #81) -- the same semantics FlagSB carries in the legacy BAG Preparations.xml. The BAG publication was correct all along: the live feed has costShare 40 on 385 of 10'388 packages (222 preparations), 10 on the rest, and never mixes the two within one preparation.
|
|
3
|
+
|
|
4
|
+
=== 3.0.30 / 11.07.2026
|
|
5
|
+
* 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).
|
|
6
|
+
|
|
7
|
+
=== 3.0.29 / 07.07.2026
|
|
8
|
+
* 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.
|
|
9
|
+
|
|
1
10
|
=== 3.0.28 / 30.06.2026
|
|
2
11
|
* 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
12
|
|
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.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
GTIN,Mediname
|
|
2
|
+
7680680690017,EZETIMIB ROSUVA Spirig HC Filmtab 10/10mg 30 Stk
|
|
3
|
+
7680680690024,EZETIMIB ROSUVA Spirig HC Filmtab 10/10mg 100 Stk
|
|
4
|
+
7680680690031,EZETIMIB ROSUVA Spirig HC Filmtab 10/20mg 30 Stk
|
|
5
|
+
7680680690048,EZETIMIB ROSUVA Spirig HC Filmtab 10/20mg 100 Stk
|
|
6
|
+
7680680690055,EZETIMIB ROSUVA Spirig HC Filmtab 10/5mg 30 Stk
|
|
7
|
+
7680680690062,EZETIMIB ROSUVA Spirig HC Filmtab 10/5mg 100 Stk
|
|
8
|
+
7680681050018,EZETIMIB ATORVAST Spirig HC Tab 10/10mg 30 Stk
|
|
9
|
+
7680681050025,EZETIMIB ATORVAST Spirig HC Tab 10/10mg 90 Stk
|
|
10
|
+
7680681050032,EZETIMIB ATORVAST Spirig HC Tab 10/20mg 30 Stk
|
|
11
|
+
7680681050049,EZETIMIB ATORVAST Spirig HC Tab 10/20mg 90 Stk
|
|
12
|
+
7680681050056,EZETIMIB ATORVAST Spirig HC Tab 10/40mg 30 Stk
|
|
13
|
+
7680681050063,EZETIMIB ATORVAST Spirig HC Tab 10/40mg 90 Stk
|
|
14
|
+
7680681050070,EZETIMIB ATORVAST Spirig HC Tab 10/80mg 30 Stk
|
|
15
|
+
7680681050087,EZETIMIB ATORVAST Spirig HC Tab 10/80mg 90 Stk
|
|
16
|
+
7680658280011,ESOMEPRAZOL Spirig HC Filmtabl 20 mg 14 Stk
|
|
17
|
+
7680658280028,ESOMEPRAZOL Spirig HC Filmtabl 20 mg 30 Stk
|
|
18
|
+
7680658280035,ESOMEPRAZOL Spirig HC Filmtabl 20 mg 60 Stk
|
|
19
|
+
7680658280042,ESOMEPRAZOL Spirig HC Filmtabl 20 mg 100 Stk
|
|
20
|
+
7680658280066,ESOMEPRAZOL Spirig HC Filmtabl 40 mg 14 Stk
|
|
21
|
+
7680658280073,ESOMEPRAZOL Spirig HC Filmtabl 40 mg 30 Stk
|
|
22
|
+
7680658280080,ESOMEPRAZOL Spirig HC Filmtabl 40 mg 60 Stk
|
|
23
|
+
7680658280097,ESOMEPRAZOL Spirig HC Filmtabl 40 mg 100 Stk
|
|
24
|
+
7680671390032,FAMPYRA Ret Tabl 10 mg 4x14 Stk
|
|
25
|
+
7680665940014,GABAPENTIN Spirig HC Kaps 100 mg 50 Stk
|
|
26
|
+
7680665940021,GABAPENTIN Spirig HC Kaps 100 mg 100 Stk
|
|
27
|
+
7680665940038,GABAPENTIN Spirig HC Kaps 300 mg 50 Stk
|
|
28
|
+
7680665940045,GABAPENTIN Spirig HC Kaps 300 mg 100 Stk
|
|
29
|
+
7680665940052,GABAPENTIN Spirig HC Kaps 400 mg 50 Stk
|
|
30
|
+
7680665940069,GABAPENTIN Spirig HC Kaps 400 mg 100 Stk
|
|
31
|
+
7680656721011,METOJECT Autoinjektor 7.5 mg/0.15 ml Fertpen 1 Stk
|
|
32
|
+
7680656721066,METOJECT Autoinjektor 10 mg/0.2 ml Fertpen 1 Stk
|
|
33
|
+
7680656721110,METOJECT Autoinjektor 12.5 mg/0.25 ml Fertpen 1 Stk
|
|
34
|
+
7680656721165,METOJECT Autoinjektor 15 mg/0.3 ml Fertpen 1 Stk
|
|
35
|
+
7680656721219,METOJECT Autoinjektor 17.5 mg/0.35 ml Fertpen 1 Stk
|
|
36
|
+
7680656721264,METOJECT Autoinjektor 20 mg/0.4 ml Fertpen 1 Stk
|
|
37
|
+
7680656721318,METOJECT Autoinjektor 22.5 mg/0.45 ml Fertpen 1 Stk
|
|
38
|
+
7680656721363,METOJECT Autoinjektor 25 mg/0.5 ml Fertpen 1 Stk
|
|
39
|
+
7680298120210,ELTROXIN LF Tabl 50 µg 100 Stk
|
|
40
|
+
7680298120012,ELTROXIN LF Tabl 100 µg 100 Stk
|
|
41
|
+
7680625680042,CETIRIZIN Spirig HC Filmtabl 10 mg 10 Stk
|
|
42
|
+
7680625680073,CETIRIZIN Spirig HC Filmtabl 10 mg 30 Stk
|
|
43
|
+
7680625680080,CETIRIZIN Spirig HC Filmtabl 10 mg 50 Stk
|
|
44
|
+
7680652800017,ATOVAQUON PLUS Spirig HC Filmtabl 250 mg / 100 mg 12 Stk
|
|
45
|
+
7680652800024,ATOVAQUON PLUS Spirig HC Filmtabl 250 mg / 100 mg 24 Stk
|
|
46
|
+
7680696960012,MOUNJARO KwikPen Inj Lös 2.5 mg/0.6 ml 1 Stk
|
|
47
|
+
7680696960029,MOUNJARO KwikPen Inj Lös 5 mg/0.6 ml 1 Stk
|
|
48
|
+
7680696960036,MOUNJARO KwikPen Inj Lös 7.5 mg/0.6 ml 1 Stk
|
|
49
|
+
7680696960043,MOUNJARO KwikPen Inj Lös 10 mg/0.6 ml 1 Stk
|
|
50
|
+
7680696960050,MOUNJARO KwikPen Inj Lös 12.5 mg/0.6 ml 1 Stk
|
|
51
|
+
7680696960067,MOUNJARO KwikPen Inj Lös 15 mg/0.6 ml 1 Stk
|
|
52
|
+
7680689680019,MELATONIN Spirig HC Ret Tabl 2 mg 21 Stk
|
|
53
|
+
7680689680026,MELATONIN Spirig HC Ret Tabl 2 mg 100 Stk
|
|
54
|
+
7680623620019,MONTELUKAST Mepha Lactab 10 mg 28 Stk
|
|
55
|
+
7680672570013,RINVOQ Ret Tabl 15 mg 28 Stk
|
|
56
|
+
7680672570037,RINVOQ Ret Tabl 30 mg 28 Stk
|
|
57
|
+
7680672570044,RINVOQ Ret Tabl 45 mg 28 Stk
|
data/lib/oddb2xml/builder.rb
CHANGED
|
@@ -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
|
data/lib/oddb2xml/downloader.rb
CHANGED
|
@@ -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
|
|
@@ -832,16 +832,19 @@ module Oddb2xml
|
|
|
832
832
|
end
|
|
833
833
|
item[:packages][ean13][:limitation_points] = ""
|
|
834
834
|
|
|
835
|
-
# Map
|
|
835
|
+
# Map the BAG costShare percentage to the deductible flags. "Y"
|
|
836
|
+
# means *erhoehter* Selbstbehalt (40%, SLOPLUS 1 / DEDUCTIBLE 40),
|
|
837
|
+
# "N" the ordinary one (10%, SLOPLUS 2 / DEDUCTIBLE 10) -- the same
|
|
838
|
+
# semantics FlagSB carries in the legacy BAG Preparations.xml.
|
|
836
839
|
if pac.CostShare
|
|
837
840
|
case pac.CostShare
|
|
838
|
-
when
|
|
841
|
+
when 40
|
|
839
842
|
item[:deductible] = "Y"
|
|
840
843
|
when 20
|
|
844
|
+
# Transitional 20% rate, see issue #81. Kept for old feeds.
|
|
841
845
|
item[:deductible20] = "Y"
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
item[:deductible] = "Y" # Fallback to standard deductible
|
|
846
|
+
else # 10 (the vast majority) and anything unexpected
|
|
847
|
+
item[:deductible] = "N"
|
|
845
848
|
end
|
|
846
849
|
end
|
|
847
850
|
|
data/lib/oddb2xml/options.rb
CHANGED
|
@@ -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"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require "csv"
|
|
2
|
+
require "oddb2xml/downloader"
|
|
3
|
+
|
|
4
|
+
module Oddb2xml
|
|
5
|
+
# Preferred German article names from the "Rogger Mediliste" — the
|
|
6
|
+
# name-conflict list maintained by Frau Rogger (Vitabyte/Zur Rose, task
|
|
7
|
+
# #OX-5985-1594). The source of truth is the shared Google Sheet
|
|
8
|
+
# "Rogger Mediliste" (GTIN,Mediname); RoggerDownloader fetches its CSV
|
|
9
|
+
# export directly, so sheet edits reach the feeds without any release step.
|
|
10
|
+
# A bundled copy under data/ serves as offline fallback (refresh it at
|
|
11
|
+
# release time when the sheet changed). A response that is not the expected
|
|
12
|
+
# CSV (e.g. a Google sign-in page when the sheet is not link-shared) is
|
|
13
|
+
# rejected and the fallback engages.
|
|
14
|
+
#
|
|
15
|
+
# Activated with -r/--rogger: for every GTIN on the list the German
|
|
16
|
+
# description coming from Refdata is replaced by the list's Mediname. The
|
|
17
|
+
# list is German-only, so FR/IT descriptions are left untouched. Applied as
|
|
18
|
+
# the last step of Builder#apply_refdata_description_cleanups!, so it sees
|
|
19
|
+
# (and wins over) the issue-#112 Refdata cleanups.
|
|
20
|
+
module RoggerNames
|
|
21
|
+
DATA_DIR = File.expand_path(File.join(__dir__, "..", "..", "data"))
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# Returns a Hash keyed by the 13-digit GTIN (String):
|
|
26
|
+
# "7680672570037" => "RINVOQ Ret Tabl 30 mg 28 Stk"
|
|
27
|
+
# Returns {} if the data cannot be obtained (never raises — the rest of
|
|
28
|
+
# the build must proceed).
|
|
29
|
+
def load(options = {})
|
|
30
|
+
map = parse(source(options))
|
|
31
|
+
Oddb2xml.log "RoggerNames: #{map.size} preferred names loaded"
|
|
32
|
+
map
|
|
33
|
+
rescue => error
|
|
34
|
+
Oddb2xml.log "RoggerNames: disabled (#{error.class}: #{error.message})"
|
|
35
|
+
{}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Download the Google Sheet CSV export; fall back to the bundled copy
|
|
39
|
+
# under data/ when the download is unavailable (offline, allow-list proxy)
|
|
40
|
+
# or does not look like the expected CSV (sheet not link-shared, Google
|
|
41
|
+
# error/sign-in page).
|
|
42
|
+
def source(options)
|
|
43
|
+
content = nil
|
|
44
|
+
begin
|
|
45
|
+
content = RoggerDownloader.new(options).download
|
|
46
|
+
rescue => error
|
|
47
|
+
Oddb2xml.log "RoggerNames: download of rogger_liste.csv failed (#{error.class}: #{error.message})"
|
|
48
|
+
end
|
|
49
|
+
unless rogger_csv?(content)
|
|
50
|
+
bundled = File.join(DATA_DIR, "rogger_liste.csv")
|
|
51
|
+
if File.exist?(bundled)
|
|
52
|
+
Oddb2xml.log "RoggerNames: using bundled rogger_liste.csv"
|
|
53
|
+
content = File.read(bundled, encoding: "UTF-8")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
content
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# True when the content is the expected sheet export: a CSV whose header
|
|
60
|
+
# row carries the GTIN and Mediname columns (rejects empty bodies and
|
|
61
|
+
# HTML sign-in/error pages).
|
|
62
|
+
def rogger_csv?(content)
|
|
63
|
+
return false if content.nil?
|
|
64
|
+
header = content.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
65
|
+
.sub(/\A\xEF\xBB\xBF/, "").lines.first.to_s
|
|
66
|
+
/GTIN/i.match?(header) && /Mediname/i.match?(header)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse(csv_string)
|
|
70
|
+
map = {}
|
|
71
|
+
return map if csv_string.nil?
|
|
72
|
+
# The list carries non-ASCII (e.g. "µg"); guard against a US-ASCII
|
|
73
|
+
# default external encoding.
|
|
74
|
+
csv_string = csv_string.dup.force_encoding(Encoding::UTF_8) unless csv_string.encoding == Encoding::UTF_8
|
|
75
|
+
return map if csv_string.strip.empty?
|
|
76
|
+
CSV.parse(csv_string, headers: true) do |row|
|
|
77
|
+
gtin = row["GTIN"].to_s.strip.rjust(13, "0")
|
|
78
|
+
name = row["Mediname"].to_s.strip
|
|
79
|
+
next unless /\A\d{13}\z/.match?(gtin)
|
|
80
|
+
next if name.empty?
|
|
81
|
+
map[gtin] = name
|
|
82
|
+
end
|
|
83
|
+
map
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/oddb2xml/version.rb
CHANGED
data/scripts/run_oddb2xml.sh
CHANGED
|
@@ -81,62 +81,70 @@ rm -rf "$BUILD_DIR"
|
|
|
81
81
|
mkdir -p "$BUILD_DIR"
|
|
82
82
|
cd "$BUILD_DIR"
|
|
83
83
|
|
|
84
|
-
# 3.
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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=()
|
|
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
|
|
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
|
-
|
|
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/fhir_spec.rb
CHANGED
|
@@ -102,6 +102,53 @@ describe "FHIR Indikationscode support" do
|
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
describe Oddb2xml::FhirExtractor, "Selbstbehalt (costShare)" do
|
|
106
|
+
# deductible "N" => SLOPLUS 2 / DEDUCTIBLE 10 (ordinary 10% Selbstbehalt),
|
|
107
|
+
# "Y" => SLOPLUS 1 / DEDUCTIBLE 40 (erhoehter Selbstbehalt).
|
|
108
|
+
def extract_with_cost_share(percent)
|
|
109
|
+
bundle = JSON.parse(File.read(cyramza_fixture))
|
|
110
|
+
bundle["entry"].each do |entry|
|
|
111
|
+
res = entry["resource"]
|
|
112
|
+
next unless res["resourceType"] == "RegulatedAuthorization"
|
|
113
|
+
Array(res["extension"]).each do |ext|
|
|
114
|
+
next unless ext["url"].to_s.include?("reimbursementSL")
|
|
115
|
+
Array(ext["extension"]).each do |sub|
|
|
116
|
+
sub["valueInteger"] = percent if sub["url"] == "costShare"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
file = Tempfile.new(["cyramza-costshare", ".ndjson"])
|
|
122
|
+
begin
|
|
123
|
+
file.write(JSON.generate(bundle))
|
|
124
|
+
file.flush
|
|
125
|
+
described_class.new(file.path).to_hash.values.first
|
|
126
|
+
ensure
|
|
127
|
+
file.close
|
|
128
|
+
file.unlink
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "maps the ordinary 10% Selbstbehalt to deductible N" do
|
|
133
|
+
# The live BAG feed carries costShare 10 for ~97% of all packages, so a
|
|
134
|
+
# 10 that ends up as "Y" makes every article claim a 40% Selbstbehalt.
|
|
135
|
+
item = extract_with_cost_share(10)
|
|
136
|
+
expect(item[:deductible]).to eq("N")
|
|
137
|
+
expect(item[:deductible20]).to eq("")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "maps the raised 40% Selbstbehalt to deductible Y" do
|
|
141
|
+
item = extract_with_cost_share(40)
|
|
142
|
+
expect(item[:deductible]).to eq("Y")
|
|
143
|
+
expect(item[:deductible20]).to eq("")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "maps the transitional 20% Selbstbehalt to deductible20 Y" do
|
|
147
|
+
item = extract_with_cost_share(20)
|
|
148
|
+
expect(item[:deductible20]).to eq("Y")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
105
152
|
describe Oddb2xml::FhirExtractor, "limitation text resolution" do
|
|
106
153
|
# Build language-variant copies of the Cyramza fixture in-memory:
|
|
107
154
|
# the live FHIR feed never stores limitation text inline, only a
|
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
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "oddb2xml/rogger_names"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Oddb2xml::RoggerNames do
|
|
7
|
+
let(:rogger_csv) {
|
|
8
|
+
<<~CSV
|
|
9
|
+
GTIN,Mediname
|
|
10
|
+
7680672570037,RINVOQ Ret Tabl 30 mg 28 Stk
|
|
11
|
+
7680665940021,GABAPENTIN Spirig HC Kaps 100 mg 100 Stk
|
|
12
|
+
7680625680042,CETIRIZIN Spirig HC Filmtabl 10 mg 10 Stk
|
|
13
|
+
not-a-gtin,BROKEN ROW
|
|
14
|
+
7680656721066,
|
|
15
|
+
CSV
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe ".parse" do
|
|
19
|
+
subject(:map) { described_class.parse(rogger_csv) }
|
|
20
|
+
|
|
21
|
+
it "maps 13-digit GTINs to the preferred German name" do
|
|
22
|
+
expect(map["7680672570037"]).to eq "RINVOQ Ret Tabl 30 mg 28 Stk"
|
|
23
|
+
expect(map["7680665940021"]).to eq "GABAPENTIN Spirig HC Kaps 100 mg 100 Stk"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "skips rows with a malformed GTIN or an empty name" do
|
|
27
|
+
expect(map.size).to eq 3
|
|
28
|
+
expect(map).not_to have_key("7680656721066")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "returns {} for blank input" do
|
|
32
|
+
expect(described_class.parse("")).to eq({})
|
|
33
|
+
expect(described_class.parse(nil)).to eq({})
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe ".rogger_csv?" do
|
|
38
|
+
it "accepts the sheet export header and rejects HTML/empty bodies" do
|
|
39
|
+
expect(described_class.rogger_csv?(rogger_csv)).to be true
|
|
40
|
+
expect(described_class.rogger_csv?("\xEF\xBB\xBFGTIN,Mediname\n")).to be true
|
|
41
|
+
expect(described_class.rogger_csv?(nil)).to be false
|
|
42
|
+
expect(described_class.rogger_csv?("")).to be false
|
|
43
|
+
expect(described_class.rogger_csv?("<!DOCTYPE html><html>Sign in - Google Accounts</html>")).to be false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe ".load" do
|
|
48
|
+
it "falls back to the bundled data/rogger_liste.csv when the download fails" do
|
|
49
|
+
flexmock(Oddb2xml::RoggerDownloader).new_instances.should_receive(:download).and_raise(SocketError)
|
|
50
|
+
map = described_class.load({})
|
|
51
|
+
expect(map).not_to be_empty
|
|
52
|
+
expect(map.keys).to all(match(/\A\d{13}\z/))
|
|
53
|
+
expect(map["7680672570037"]).to eq "RINVOQ Ret Tabl 30 mg 28 Stk"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "falls back to the bundled CSV when the response is not the sheet export" do
|
|
57
|
+
flexmock(Oddb2xml::RoggerDownloader).new_instances.should_receive(:download)
|
|
58
|
+
.and_return("<!DOCTYPE html><html>Sign in - Google Accounts</html>")
|
|
59
|
+
map = described_class.load({})
|
|
60
|
+
expect(map["7680672570037"]).to eq "RINVOQ Ret Tabl 30 mg 28 Stk"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "never raises, returning {} when no source is available" do
|
|
64
|
+
flexmock(Oddb2xml::RoggerDownloader).new_instances.should_receive(:download).and_raise(SocketError)
|
|
65
|
+
flexmock(described_class).should_receive(:source).and_return(nil)
|
|
66
|
+
expect(described_class.load({})).to eq({})
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe "Builder#apply_rogger_name_overrides!" do
|
|
71
|
+
it "replaces only the German description of listed GTINs" do
|
|
72
|
+
builder = Oddb2xml::Builder.new({})
|
|
73
|
+
builder.rogger_names = {"7680672570037" => "RINVOQ Ret Tabl 30 mg 28 Stk"}
|
|
74
|
+
builder.refdata = {
|
|
75
|
+
"7680672570037" => {ean13: "7680672570037", no8: "67257003",
|
|
76
|
+
desc_de: "RINVOQ Retardtabletten 30 mg 28 Stk",
|
|
77
|
+
desc_fr: "RINVOQ cpr ret 30 mg 28 pce"},
|
|
78
|
+
"7680671390032" => {ean13: "7680671390032", no8: "67139003",
|
|
79
|
+
desc_de: "FAMPYRA Ret Tabl 10 mg 4x14 Stk",
|
|
80
|
+
desc_fr: "FAMPYRA cpr ret 10 mg 4x14 pce"}
|
|
81
|
+
}
|
|
82
|
+
builder.send(:apply_rogger_name_overrides!)
|
|
83
|
+
expect(builder.refdata["7680672570037"][:desc_de]).to eq "RINVOQ Ret Tabl 30 mg 28 Stk"
|
|
84
|
+
expect(builder.refdata["7680672570037"][:desc_fr]).to eq "RINVOQ cpr ret 30 mg 28 pce"
|
|
85
|
+
expect(builder.refdata["7680671390032"][:desc_de]).to eq "FAMPYRA Ret Tabl 10 mg 4x14 Stk"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: oddb2xml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0.
|
|
4
|
+
version: 3.0.31
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yasuhiro Asaka, Zeno R.R. Davatz, Niklaus Giger
|
|
@@ -468,6 +468,7 @@ files:
|
|
|
468
468
|
- data/gal_groups.yaml
|
|
469
469
|
- data/gtin2ignore.yaml
|
|
470
470
|
- data/product_overrides.yaml
|
|
471
|
+
- data/rogger_liste.csv
|
|
471
472
|
- data/wala_arzneimittel.csv
|
|
472
473
|
- data/weleda_arzneimittel.csv
|
|
473
474
|
- dokumentation_calc.textile
|
|
@@ -489,6 +490,7 @@ files:
|
|
|
489
490
|
- lib/oddb2xml/parslet_compositions.rb
|
|
490
491
|
- lib/oddb2xml/proxy_check.rb
|
|
491
492
|
- lib/oddb2xml/refdata_cleanup.rb
|
|
493
|
+
- lib/oddb2xml/rogger_names.rb
|
|
492
494
|
- lib/oddb2xml/semantic_check.rb
|
|
493
495
|
- lib/oddb2xml/util.rb
|
|
494
496
|
- lib/oddb2xml/version.rb
|
|
@@ -579,6 +581,7 @@ files:
|
|
|
579
581
|
- spec/parslet_spec.rb
|
|
580
582
|
- spec/proxy_check_spec.rb
|
|
581
583
|
- spec/refdata_cleanup_spec.rb
|
|
584
|
+
- spec/rogger_names_spec.rb
|
|
582
585
|
- spec/spec_helper.rb
|
|
583
586
|
- spec/weleda_sl_spec.rb
|
|
584
587
|
- test_options.rb
|
|
@@ -684,5 +687,6 @@ test_files:
|
|
|
684
687
|
- spec/parslet_spec.rb
|
|
685
688
|
- spec/proxy_check_spec.rb
|
|
686
689
|
- spec/refdata_cleanup_spec.rb
|
|
690
|
+
- spec/rogger_names_spec.rb
|
|
687
691
|
- spec/spec_helper.rb
|
|
688
692
|
- spec/weleda_sl_spec.rb
|