oddb2xml 3.0.30 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd5aba5cd43be1930a5499b102b5a1921454d422de9aa13de227e92ad1b5cae6
4
- data.tar.gz: 1861fc5dba9e46179aaa4f69e64fa6e10288aeafff677ec591156926a54e8633
3
+ metadata.gz: e8ae3ea587547caf78cba9d778e08e6474535800d111cf484f981f5eb86cc7c2
4
+ data.tar.gz: cf33dfe5d3b549fa1c2fc1542cc0f7754e369a8d50c757705c869ba119b245b5
5
5
  SHA512:
6
- metadata.gz: eb67bef331b97a7f3b347d342c9c2c8128fe9d53154363b89d3868d97d61d2d6469568aa78fb597129f33687af68c2ef38ecb227a6e67d807b8e06df220e4694
7
- data.tar.gz: b4835b7045e30b1e24a84afd96c499db2b09fe0ec0175ea8b8e82adfe9a7b4a096237e6be6879cb9636f9608a0161346dda37b1606abf38d62762d858375b056
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
 
@@ -85,8 +60,3 @@ Only the scripts are git-tracked; the generated `index.html`/`logo.svg` and the
85
60
  - Test fixtures: `spec/data/` (sample files), `spec/fixtures/vcr_cassettes/` (recorded HTTP responses)
86
61
  - `spec/spec_helper.rb` defines test constants (GTINs) and configures VCR to avoid real HTTP calls during tests
87
62
  - CI runs on Ruby 3.0, 3.1, 3.2
88
-
89
- ## Ruby Version
90
-
91
- - Minimum: Ruby >= 2.5.0 (gemspec)
92
- - 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.30)
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.3)
92
+ nokogiri (1.19.4)
93
93
  mini_portile2 (~> 2.8.2)
94
94
  racc (~> 1.4)
95
- nokogiri (1.19.3-arm64-darwin)
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.43)
213
+ yard (0.9.44)
214
214
 
215
215
  PLATFORMS
216
216
  arm64-darwin-25
data/History.txt CHANGED
@@ -1,3 +1,6 @@
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
+
1
4
  === 3.0.30 / 11.07.2026
2
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).
3
6
 
@@ -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
@@ -832,16 +832,19 @@ module Oddb2xml
832
832
  end
833
833
  item[:packages][ean13][:limitation_points] = ""
834
834
 
835
- # Map cost_share to deductible flags
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 10
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
- when 40
843
- # New value - might need new field or special handling
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
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Oddb2xml
2
- VERSION = "3.0.30"
2
+ VERSION = "3.0.31"
3
3
  end
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
@@ -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,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oddb2xml
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.30
4
+ version: 3.0.31
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yasuhiro Asaka, Zeno R.R. Davatz, Niklaus Giger
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-07-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rubyzip
@@ -469,6 +468,7 @@ files:
469
468
  - data/gal_groups.yaml
470
469
  - data/gtin2ignore.yaml
471
470
  - data/product_overrides.yaml
471
+ - data/rogger_liste.csv
472
472
  - data/wala_arzneimittel.csv
473
473
  - data/weleda_arzneimittel.csv
474
474
  - dokumentation_calc.textile
@@ -490,6 +490,7 @@ files:
490
490
  - lib/oddb2xml/parslet_compositions.rb
491
491
  - lib/oddb2xml/proxy_check.rb
492
492
  - lib/oddb2xml/refdata_cleanup.rb
493
+ - lib/oddb2xml/rogger_names.rb
493
494
  - lib/oddb2xml/semantic_check.rb
494
495
  - lib/oddb2xml/util.rb
495
496
  - lib/oddb2xml/version.rb
@@ -580,6 +581,7 @@ files:
580
581
  - spec/parslet_spec.rb
581
582
  - spec/proxy_check_spec.rb
582
583
  - spec/refdata_cleanup_spec.rb
584
+ - spec/rogger_names_spec.rb
583
585
  - spec/spec_helper.rb
584
586
  - spec/weleda_sl_spec.rb
585
587
  - test_options.rb
@@ -591,7 +593,6 @@ homepage: https://github.com/zdavatz/oddb2xml
591
593
  licenses:
592
594
  - GPL-3.0-only
593
595
  metadata: {}
594
- post_install_message:
595
596
  rdoc_options: []
596
597
  require_paths:
597
598
  - lib
@@ -606,8 +607,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
606
607
  - !ruby/object:Gem::Version
607
608
  version: '0'
608
609
  requirements: []
609
- rubygems_version: 3.5.22
610
- signing_key:
610
+ rubygems_version: 3.6.9
611
611
  specification_version: 4
612
612
  summary: oddb2xml creates xml files.
613
613
  test_files:
@@ -687,5 +687,6 @@ test_files:
687
687
  - spec/parslet_spec.rb
688
688
  - spec/proxy_check_spec.rb
689
689
  - spec/refdata_cleanup_spec.rb
690
+ - spec/rogger_names_spec.rb
690
691
  - spec/spec_helper.rb
691
692
  - spec/weleda_sl_spec.rb