oddb2xml 3.0.7 → 3.0.8

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: 6c01ea6ec31975e297588db6fe417753aed570dc5ddf6123facf03a21224adf5
4
- data.tar.gz: 74074f93366162137d62bca64833e98564137f80dde6481f567dd844b0d0ae14
3
+ metadata.gz: a5391f07cc4388c80d6347856c433d40002143f8a368af6d45ceb3f0662861bf
4
+ data.tar.gz: c02c407165679d39512f8d23de883f870c0a9d89d723c85f1bd3172b73f3bbd4
5
5
  SHA512:
6
- metadata.gz: 1ab9e25407b856b59ae06f1ec07ae93cf462ba0dfda587cf7f6e8dd22440e44df22ca67e49d4144ac60e27e7fc9cc8eccd7ee0b5888cec0ee32782a02cf0b48c
7
- data.tar.gz: da022887409106853d0c8c114a337edcae566d5573f7febc8e34642e923269790192d87519144324e90bf93df78f187b12bcf6f7ca28429781d23ff28dcaff99
6
+ metadata.gz: 10f45b9f5aa5095073260bc034be15d643484fbb9e717b847e839ad04a508f121aa6c6ed6ab8efd0e64c7e3eebea7b839182dd5af70cbe9a30b38f7334f2ebd8
7
+ data.tar.gz: 69c6642e96ff6abb51ded2519f770e69e09d49a8b8b52c7121f287f47e34fccc4679f8643246d85ce74f32702cb0d26e53992140d19d6c0c4fcaab272bc459e1
data/CLAUDE.md CHANGED
@@ -47,7 +47,7 @@ The system follows a **download → extract → build → compress** pipeline:
47
47
 
48
48
  6. **Compressor** (`lib/oddb2xml/compressor.rb`) — Optional ZIP/TAR.GZ output compression.
49
49
 
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. Parses `ClinicalUseDefinition` resources and combines each indication's `.NN` id-suffix with the reimbursement RA's `FOPHDossierNumber` to construct the BAG **Indikationscode** (`XXXXX.NN`); exposed as `item[:indication_codes]` and per-package `:indication_codes` (each entry a `{code:, cud_id:, text:}` hash). 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).
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. Parses `ClinicalUseDefinition` resources and combines each indication's `.NN` id-suffix with the reimbursement RA's `FOPHDossierNumber` to construct the BAG **Indikationscode** (`XXXXX.NN`); exposed as `item[:indication_codes]` and per-package `:indication_codes` (each entry a `{code:, cud_id:, text:}` hash). 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)).
51
51
 
52
52
  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 the doubled-dose template bug (`X mg / X mg / Stk`). Called from `Builder#apply_refdata_description_cleanups!` at the start of `prepare_articles`. See GitHub issue #112 for the catalogue.
53
53
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oddb2xml (3.0.7)
4
+ oddb2xml (3.0.8)
5
5
  csv
6
6
  htmlentities
7
7
  httpi
data/History.txt CHANGED
@@ -1,3 +1,6 @@
1
+ === 3.0.8 / 28.05.2026
2
+ * FHIR: fix empty <DescriptionDe/Fr/It> on every <Limitation>. The live BAG FHIR feed does not carry limitation text inline; it stores a `limitationIndication` reference to a `ClinicalUseDefinition` whose `indication.diseaseSymptomProcedure.concept.text` holds the actual text (one per language). FR/IT come from the per-language NDJSON files and are merged via the CUD id. Result on the live feed: 9'108 / 9'108 limitations (100%) now carry text, up from 0% (issue #116).
3
+
1
4
  === 3.0.7 / 06.05.2026
2
5
  * oddb_product.xml: emit one <INDICATION_CODE code="XXXXX.NN" cud_id="DRUG.NN">limitation text</INDICATION_CODE> child per indication on each <PRD>. Source: FHIR ClinicalUseDefinition resources joined with the reimbursement RegulatedAuthorization's FOPHDossierNumber. Counted on the live FOPH feed: 539 products carry a total of 1,293 codes; all have non-empty indication text. Resolves the missing wiring between FhirExtractor and Builder noted in 3.0.6 (issue #113).
3
6
 
data/README.md CHANGED
@@ -51,7 +51,7 @@ HIN (http://hin.ch) creates daily the actual file. They can be downloaded from `
51
51
  see `--help`.
52
52
 
53
53
  ```
54
- /opt/src/oddb2xml/bin/oddb2xml version 3.0.7
54
+ /opt/src/oddb2xml/bin/oddb2xml version 3.0.8
55
55
  Usage:
56
56
  oddb2xml [option]
57
57
  produced files are found under data
@@ -329,6 +329,30 @@ drugs from **2026-07-01**; from **2027-01-01** insurers may reject
329
329
  invoices without it. See issue
330
330
  [#113](https://github.com/zdavatz/oddb2xml/issues/113).
331
331
 
332
+ ## Limitation texts in `--fhir` mode
333
+
334
+ In 3.0.8 we fixed empty `<DescriptionDe/Fr/It>` on every `<Limitation>`
335
+ in the FHIR-built output. The live BAG FHIR feed does **not** carry
336
+ limitation text inline on the `regulatedAuthorization-limitation`
337
+ extension. Instead the extension holds a `limitationIndication`
338
+ reference to a `ClinicalUseDefinition` whose
339
+ `indication.diseaseSymptomProcedure.concept.text` is the actual text:
340
+
341
+ ```
342
+ RegulatedAuthorization/65839
343
+ └─ indication.extension[limitation]
344
+ ├─ status, statusDate, period, firstLimitationDate
345
+ └─ limitationIndication → ClinicalUseDefinition/NORDIMET
346
+ └─ concept.text = "Wird nicht ..."
347
+ ```
348
+
349
+ The CUDs are identical in `id` across the three per-language NDJSON
350
+ files (`foph-sl-export-latest-{de,fr,it}.ndjson`); only the
351
+ `concept.text` differs. `FhirExtractor` resolves DE from the primary
352
+ file and merges FR/IT in via the same CUD id. Coverage on the live
353
+ feed went from 0 / 9'108 to 9'108 / 9'108 (100 %). See issue
354
+ [#116](https://github.com/zdavatz/oddb2xml/issues/116).
355
+
332
356
  ## Refdata data-quality compensation
333
357
 
334
358
  Refdata.Articles.xml from `files.refdata.ch` ships with a number of recurring
@@ -167,6 +167,16 @@ module Oddb2xml
167
167
  parse_entries
168
168
  end
169
169
 
170
+ # Lookup map: CUD id (e.g. "NORDIMET" or "GLIVEC.01") => indication text.
171
+ # Used to resolve limitation texts that are stored as a reference on
172
+ # the RegulatedAuthorization rather than inline.
173
+ def cud_text_by_id
174
+ @cud_text_by_id ||= @clinical_use_definitions.each_with_object({}) do |cud, acc|
175
+ next unless cud.id && cud.text
176
+ acc[cud.id] = cud.text
177
+ end
178
+ end
179
+
170
180
  private
171
181
 
172
182
  def parse_entries
@@ -383,7 +393,11 @@ module Oddb2xml
383
393
  when "statusDate"
384
394
  limitation[:status_date] = sub_ext["valueDate"]
385
395
  when "limitationText"
396
+ # Not present in the live FHIR feed — kept for forward-compat.
386
397
  limitation[:text] = sub_ext["valueString"]
398
+ when "limitationIndication"
399
+ ref = sub_ext.dig("valueReference", "reference")
400
+ limitation[:cud_ref] = ref&.sub(%r{\A.*ClinicalUseDefinition/}, "")
387
401
  when "period"
388
402
  limitation[:start_date] = sub_ext.dig("valuePeriod", "start")
389
403
  limitation[:end_date] = sub_ext.dig("valuePeriod", "end")
@@ -467,11 +481,15 @@ module Oddb2xml
467
481
 
468
482
  # Find prices and additional data for this package
469
483
  pack.Prices = create_prices_for_package(bundle, pkg)
470
-
484
+
471
485
  # Add limitations and cost share
472
486
  pack.Limitations = create_limitations_for_package(bundle, pkg)
473
487
  pack.CostShare = get_cost_share_for_package(bundle, pkg)
474
488
 
489
+ # Per-language CUD text map so merge_language can fill in
490
+ # DescriptionFr / DescriptionIt for limitations without re-parsing.
491
+ pack.CudTextById = bundle.cud_text_by_id
492
+
475
493
  pack
476
494
  end
477
495
 
@@ -533,23 +551,32 @@ module Oddb2xml
533
551
 
534
552
  return nil unless reimbursement
535
553
  return nil if reimbursement.limitations.empty?
536
-
554
+
555
+ cud_texts = bundle.cud_text_by_id
556
+
537
557
  # Convert FHIR limitations to OpenStruct format
538
558
  limitations = OpenStruct.new
539
559
  limitations.Limitation = reimbursement.limitations.map do |lim|
560
+ # The actual limitation text lives in the referenced
561
+ # ClinicalUseDefinition (limitationIndication reference) — the
562
+ # `limitationText` sub-extension is absent in the live BAG feed.
563
+ cud_ref = lim[:cud_ref]
564
+ text_de = lim[:text] || (cud_ref && cud_texts[cud_ref]) || ""
565
+
540
566
  limitation = OpenStruct.new
541
567
  limitation.LimitationCode = "" # Not in FHIR
542
568
  limitation.LimitationType = "" # Could derive from status
543
569
  limitation.LimitationNiveau = "" # Not in FHIR
544
570
  limitation.LimitationValue = "" # Not in FHIR
545
- limitation.DescriptionDe = lim[:text] || ""
546
- limitation.DescriptionFr = "" # May need separate language version
547
- limitation.DescriptionIt = "" # May need separate language version
571
+ limitation.LimitationCudRef = cud_ref # carried through for FR/IT resolution
572
+ limitation.DescriptionDe = text_de
573
+ limitation.DescriptionFr = "" # filled by merge_language from FR bundle
574
+ limitation.DescriptionIt = "" # filled by merge_language from IT bundle
548
575
  limitation.ValidFromDate = lim[:status_date] || lim[:start_date] || ""
549
576
  limitation.ValidThruDate = lim[:end_date] || ""
550
577
  limitation
551
578
  end
552
-
579
+
553
580
  limitations
554
581
  end
555
582
 
@@ -751,6 +778,7 @@ module Oddb2xml
751
778
  desc_de: lim.DescriptionDe || "",
752
779
  desc_fr: lim.DescriptionFr || "",
753
780
  desc_it: lim.DescriptionIt || "",
781
+ cud_ref: lim.LimitationCudRef,
754
782
  vdate: lim.ValidFromDate || "",
755
783
  del: is_deleted
756
784
  }
@@ -796,6 +824,7 @@ module Oddb2xml
796
824
  name_accessor = "Name#{lang.capitalize}"
797
825
  name_key = "name_#{lang}".to_sym
798
826
  desc_key = "desc_#{lang}".to_sym
827
+ lim_desc_key = "desc_#{lang}".to_sym
799
828
 
800
829
  result.Preparations.Preparation.each do |seq|
801
830
  next unless seq && seq.Packs && seq.Packs.Pack
@@ -822,6 +851,19 @@ module Oddb2xml
822
851
  if desc && !desc.empty? && item[:packages][ean13]
823
852
  item[:packages][ean13][desc_key] = desc
824
853
  end
854
+
855
+ # Resolve FR/IT limitation texts via the CUD reference captured
856
+ # during the DE pass. The CUD id (e.g. "NORDIMET") is identical
857
+ # across languages; only the text differs.
858
+ pkg_entry = item[:packages][ean13]
859
+ cud_texts = pac.respond_to?(:CudTextById) ? pac.CudTextById : nil
860
+ if pkg_entry && cud_texts && pkg_entry[:limitations]
861
+ pkg_entry[:limitations].each do |lim|
862
+ ref = lim[:cud_ref]
863
+ text = ref && cud_texts[ref]
864
+ lim[lim_desc_key] = text if text && !text.empty?
865
+ end
866
+ end
825
867
  end
826
868
  end
827
869
  end
@@ -1,3 +1,3 @@
1
1
  module Oddb2xml
2
- VERSION = "3.0.7"
2
+ VERSION = "3.0.8"
3
3
  end
data/spec/fhir_spec.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require "spec_helper"
2
+ require "json"
3
+ require "tempfile"
2
4
  require "oddb2xml/downloader"
3
5
  require "oddb2xml/extractor"
4
6
  require "oddb2xml/fhir_support"
@@ -52,6 +54,92 @@ describe "FHIR Indikationscode support" do
52
54
  end
53
55
  end
54
56
 
57
+ describe Oddb2xml::FhirExtractor, "limitation text resolution" do
58
+ # Build language-variant copies of the Cyramza fixture in-memory:
59
+ # the live FHIR feed never stores limitation text inline, only a
60
+ # reference to a ClinicalUseDefinition whose `concept.text` differs
61
+ # per language. We translate the CUD text + MPD product name and
62
+ # write the modified bundle to a Tempfile so the multi-language
63
+ # path can be exercised end-to-end.
64
+ def language_variant(source_path, lang_code, cud_texts, product_name)
65
+ bundle = JSON.parse(File.read(source_path))
66
+ bundle["entry"].each do |entry|
67
+ res = entry["resource"]
68
+ case res["resourceType"]
69
+ when "MedicinalProductDefinition"
70
+ res["name"].each do |name|
71
+ usage = name.dig("usage", 0, "language", "coding", 0)
72
+ usage["code"] = lang_code if usage
73
+ name["productName"] = product_name
74
+ end
75
+ when "ClinicalUseDefinition"
76
+ text = cud_texts[res["id"]]
77
+ if text
78
+ res["indication"]["diseaseSymptomProcedure"]["concept"]["text"] = text
79
+ end
80
+ end
81
+ end
82
+ file = Tempfile.new(["cyramza-#{lang_code}", ".ndjson"])
83
+ file.write(JSON.generate(bundle))
84
+ file.flush
85
+ file
86
+ end
87
+
88
+ let(:fr_file) do
89
+ language_variant(
90
+ cyramza_fixture, "fr-CH",
91
+ {
92
+ "CYRAMZA.01" => "FR limitation pour CYRAMZA.01",
93
+ "CYRAMZA.02" => "FR limitation pour CYRAMZA.02"
94
+ },
95
+ "Cyramza FR"
96
+ )
97
+ end
98
+
99
+ let(:it_file) do
100
+ language_variant(
101
+ cyramza_fixture, "it-CH",
102
+ {
103
+ "CYRAMZA.01" => "IT limitazione per CYRAMZA.01",
104
+ "CYRAMZA.02" => "IT limitazione per CYRAMZA.02"
105
+ },
106
+ "Cyramza IT"
107
+ )
108
+ end
109
+
110
+ after do
111
+ [fr_file, it_file].each do |f|
112
+ f.close
113
+ f.unlink
114
+ end
115
+ end
116
+
117
+ it "fills DescriptionDe from the referenced ClinicalUseDefinition" do
118
+ data = described_class.new(cyramza_fixture).to_hash
119
+ pkg = data.values.first[:packages].values.first
120
+ texts = pkg[:limitations].map { |l| l[:desc_de] }
121
+ expect(texts).to include(start_with("In Kombination mit Paclitaxel"))
122
+ expect(texts).to include(start_with("In Kombination mit FOLFIRI"))
123
+ # CUD reference is carried through so merge_language can resolve FR/IT.
124
+ expect(pkg[:limitations].map { |l| l[:cud_ref] }).to include("CYRAMZA.01", "CYRAMZA.02")
125
+ end
126
+
127
+ it "fills DescriptionFr / DescriptionIt from the language-specific bundles" do
128
+ files = {"de" => cyramza_fixture, "fr" => fr_file.path, "it" => it_file.path}
129
+ data = described_class.new(files).to_hash
130
+ pkg = data.values.first[:packages].values.first
131
+
132
+ by_ref = pkg[:limitations].each_with_object({}) { |l, h| h[l[:cud_ref]] = l }
133
+
134
+ expect(by_ref["CYRAMZA.01"][:desc_fr]).to eq("FR limitation pour CYRAMZA.01")
135
+ expect(by_ref["CYRAMZA.02"][:desc_fr]).to eq("FR limitation pour CYRAMZA.02")
136
+ expect(by_ref["CYRAMZA.01"][:desc_it]).to eq("IT limitazione per CYRAMZA.01")
137
+ expect(by_ref["CYRAMZA.02"][:desc_it]).to eq("IT limitazione per CYRAMZA.02")
138
+ # DE text is still there.
139
+ expect(by_ref["CYRAMZA.01"][:desc_de]).to start_with("In Kombination mit Paclitaxel")
140
+ end
141
+ end
142
+
55
143
  describe Oddb2xml::Builder, "PRD INDICATION_CODE emission" do
56
144
  it "emits one <INDICATION_CODE> child per indication on the PRD" do
57
145
  items = Oddb2xml::FhirExtractor.new(cyramza_fixture).to_hash
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.7
4
+ version: 3.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yasuhiro Asaka, Zeno R.R. Davatz, Niklaus Giger