oddb2xml 3.0.4 → 3.0.6

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: 3a0a4cbd5ff288013064ffb6d756ed0961fd98e52b6960fbfd32b4328a039369
4
- data.tar.gz: 60c9943c0a7f344ecf87e04f16314da1b8a29e0801a8d7e814d265932f89e443
3
+ metadata.gz: 8b3a91c4a9ff36983011294c705fd882a5cf2a7f60f972cd1e2de61877e353db
4
+ data.tar.gz: 7eb411ee5192d1a1eec417536e62761c5cbb833a075221ae283f1de577f5a4f6
5
5
  SHA512:
6
- metadata.gz: 75abbcde0acf8e33b8489dd03fc06e6bd8796f571444b0a7f7eb3cded37a25f0938d8db07a7ef6ef0023412e3ab2336c046be3447fb5dc371dd02f06c5950e12
7
- data.tar.gz: 9cf7e4c5eecd5d708dea61b4c6e17a206f6f4eaa65742ae2ee0c7ea3a49d0b4e4ae2dad7d9ffd3c2e61191e8703693ea29b3aa194d517226f828282ff5b3f891
6
+ metadata.gz: 92721b900cb1f754d87463c8a22fd552eabceee1ec12e16aca202084bfd18fa03f4def7ea3c2232ba4598d225c49f165a9c49cf995aae54f9a655d8551cb7267
7
+ data.tar.gz: ef7383d72a3535996c70d9d723b12649b09927cff97b9575e49f15553c87c92d142c940eded2fd22d4ef8399bf6f7d2538c72e413623485150837bfa43d0faf1
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.2.0
1
+ 3.4.5
data/CLAUDE.md CHANGED
@@ -49,6 +49,8 @@ The system follows a **download → extract → build → compress** pipeline:
49
49
 
50
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.
51
51
 
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
+
52
54
  ### Key data identifiers
53
55
  - **GTIN/EAN13**: Primary article identifier (13-digit barcode)
54
56
  - **Pharmacode**: Swiss pharmacy code
@@ -68,4 +70,4 @@ YAML files in `data/` provide manual overrides and mappings: `article_overrides.
68
70
  ## Ruby Version
69
71
 
70
72
  - Minimum: Ruby >= 2.5.0 (gemspec)
71
- - Current development: Ruby 3.2.0 (`.ruby-version`)
73
+ - Current development: Ruby 3.3.6 (`.ruby-version`)
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oddb2xml (3.0.4)
4
+ oddb2xml (3.0.6)
5
+ csv
5
6
  htmlentities
6
7
  httpi
7
8
  mechanize (>= 2.8.5)
@@ -28,32 +29,38 @@ GEM
28
29
  specs:
29
30
  addressable (2.8.8)
30
31
  public_suffix (>= 2.0.2, < 8.0)
31
- akami (1.3.1)
32
+ akami (1.3.3)
33
+ base64
32
34
  gyoku (>= 0.4.0)
33
35
  nokogiri
34
- ast (2.4.2)
36
+ ast (2.4.3)
35
37
  base64 (0.3.0)
36
- builder (3.2.4)
37
- byebug (11.1.3)
38
+ bigdecimal (4.0.1)
39
+ builder (3.3.0)
40
+ byebug (13.0.0)
41
+ reline (>= 0.6.0)
38
42
  coderay (1.1.3)
39
43
  connection_pool (3.0.2)
40
- crack (0.4.5)
44
+ crack (1.0.1)
45
+ bigdecimal
41
46
  rexml
42
- diff-lcs (1.5.0)
47
+ csv (3.3.5)
48
+ diff-lcs (1.6.2)
43
49
  domain_name (0.6.20240107)
44
- flexmock (2.3.8)
50
+ flexmock (3.0.2)
45
51
  gyoku (1.4.0)
46
52
  builder (>= 2.1.2)
47
53
  rexml (~> 3.0)
48
- hashdiff (1.0.1)
49
- htmlentities (4.3.4)
54
+ hashdiff (1.2.1)
55
+ htmlentities (4.4.2)
50
56
  http-cookie (1.1.0)
51
57
  domain_name (~> 0.5)
52
58
  httpi (2.5.0)
53
59
  rack
54
60
  socksify
55
- json (2.6.3)
56
- language_server-protocol (3.17.0.3)
61
+ io-console (0.8.2)
62
+ json (2.18.1)
63
+ language_server-protocol (3.17.0.5)
57
64
  lint_roller (1.1.0)
58
65
  logger (1.7.0)
59
66
  mechanize (2.14.0)
@@ -69,84 +76,95 @@ GEM
69
76
  rubyntlm (~> 0.6, >= 0.6.3)
70
77
  webrick (~> 1.7)
71
78
  webrobots (~> 0.1.2)
72
- method_source (1.0.0)
79
+ method_source (1.1.0)
73
80
  mime-types (3.7.0)
74
81
  logger
75
82
  mime-types-data (~> 3.2025, >= 3.2025.0507)
76
83
  mime-types-data (3.2026.0203)
77
84
  mini_portile2 (2.8.9)
78
- minitar (0.9)
79
- multi_json (1.15.0)
85
+ minitar (1.1.0)
86
+ multi_json (1.19.1)
80
87
  mutex_m (0.3.0)
81
88
  net-http-digest_auth (1.4.1)
82
89
  net-http-persistent (4.0.8)
83
90
  connection_pool (>= 2.2.4, < 4)
84
91
  nkf (0.2.0)
85
- nokogiri (1.19.1)
92
+ nokogiri (1.19.2)
86
93
  mini_portile2 (~> 2.8.2)
87
94
  racc (~> 1.4)
88
- nori (2.6.0)
89
- optimist (3.1.0)
90
- ox (2.14.14)
91
- parallel (1.23.0)
92
- parser (3.2.2.3)
95
+ nokogiri (1.19.2-arm64-darwin)
96
+ racc (~> 1.4)
97
+ nori (2.7.1)
98
+ bigdecimal
99
+ optimist (3.2.1)
100
+ ox (2.14.23)
101
+ bigdecimal (>= 3.0)
102
+ parallel (1.27.0)
103
+ parser (3.3.10.2)
93
104
  ast (~> 2.4.1)
94
105
  racc
95
106
  parslet (2.0.0)
96
- pry (0.14.2)
107
+ prism (1.9.0)
108
+ pry (0.16.0)
97
109
  coderay (~> 1.1)
98
110
  method_source (~> 1.0)
99
- pry-byebug (3.8.0)
100
- byebug (~> 11.0)
101
- pry (~> 0.10)
102
- pry-doc (1.4.0)
111
+ reline (>= 0.6.0)
112
+ pry-byebug (3.12.0)
113
+ byebug (~> 13.0)
114
+ pry (>= 0.13, < 0.17)
115
+ pry-doc (1.7.0)
103
116
  pry (~> 0.11)
104
- yard (~> 0.9.11)
117
+ yard (~> 0.9.21)
105
118
  psych (3.3.4)
106
119
  public_suffix (7.0.2)
107
120
  racc (1.8.1)
108
- rack (3.2.5)
121
+ rack (3.2.6)
109
122
  rainbow (3.1.1)
110
- rake (13.0.6)
123
+ rake (13.3.1)
111
124
  rdoc (6.3.4.1)
112
- regexp_parser (2.8.1)
125
+ regexp_parser (2.11.3)
126
+ reline (0.6.3)
127
+ io-console (~> 0.5)
113
128
  rexml (3.4.4)
114
- rspec (3.12.0)
115
- rspec-core (~> 3.12.0)
116
- rspec-expectations (~> 3.12.0)
117
- rspec-mocks (~> 3.12.0)
118
- rspec-core (3.12.2)
119
- rspec-support (~> 3.12.0)
120
- rspec-expectations (3.12.3)
129
+ rspec (3.13.2)
130
+ rspec-core (~> 3.13.0)
131
+ rspec-expectations (~> 3.13.0)
132
+ rspec-mocks (~> 3.13.0)
133
+ rspec-core (3.13.6)
134
+ rspec-support (~> 3.13.0)
135
+ rspec-expectations (3.13.5)
121
136
  diff-lcs (>= 1.2.0, < 2.0)
122
- rspec-support (~> 3.12.0)
123
- rspec-mocks (3.12.6)
137
+ rspec-support (~> 3.13.0)
138
+ rspec-mocks (3.13.7)
124
139
  diff-lcs (>= 1.2.0, < 2.0)
125
- rspec-support (~> 3.12.0)
126
- rspec-support (3.12.1)
127
- rubocop (1.50.2)
140
+ rspec-support (~> 3.13.0)
141
+ rspec-support (3.13.7)
142
+ rubocop (1.84.2)
128
143
  json (~> 2.3)
144
+ language_server-protocol (~> 3.17.0.2)
145
+ lint_roller (~> 1.1.0)
129
146
  parallel (~> 1.10)
130
- parser (>= 3.2.0.0)
147
+ parser (>= 3.3.0.2)
131
148
  rainbow (>= 2.2.2, < 4.0)
132
- regexp_parser (>= 1.8, < 3.0)
133
- rexml (>= 3.2.5, < 4.0)
134
- rubocop-ast (>= 1.28.0, < 2.0)
149
+ regexp_parser (>= 2.9.3, < 3.0)
150
+ rubocop-ast (>= 1.49.0, < 2.0)
135
151
  ruby-progressbar (~> 1.7)
136
- unicode-display_width (>= 2.4.0, < 3.0)
137
- rubocop-ast (1.29.0)
138
- parser (>= 3.2.1.0)
139
- rubocop-performance (1.16.0)
140
- rubocop (>= 1.7.0, < 2.0)
141
- rubocop-ast (>= 0.4.0)
142
- ruby-ole (1.2.12.2)
152
+ unicode-display_width (>= 2.4.0, < 4.0)
153
+ rubocop-ast (1.49.0)
154
+ parser (>= 3.3.7.2)
155
+ prism (~> 1.7)
156
+ rubocop-performance (1.26.1)
157
+ lint_roller (~> 1.1)
158
+ rubocop (>= 1.75.0, < 2.0)
159
+ rubocop-ast (>= 1.47.1, < 2.0)
160
+ ruby-ole (1.2.13.1)
143
161
  ruby-progressbar (1.13.0)
144
- rubyXL (3.4.25)
162
+ rubyXL (3.4.33)
145
163
  nokogiri (>= 1.10.8)
146
164
  rubyzip (>= 1.3.0)
147
165
  rubyntlm (0.6.5)
148
166
  base64
149
- rubyzip (3.0.1)
167
+ rubyzip (3.0.2)
150
168
  savon (2.12.1)
151
169
  akami (~> 1.2)
152
170
  builder (>= 2.1.2)
@@ -156,31 +174,35 @@ GEM
156
174
  nori (~> 2.4)
157
175
  wasabi (~> 3.4)
158
176
  sax-machine (1.3.2)
159
- socksify (1.7.1)
160
- spreadsheet (1.3.0)
177
+ socksify (1.8.1)
178
+ spreadsheet (1.3.4)
179
+ bigdecimal
180
+ logger
161
181
  ruby-ole
162
- standard (1.28.5)
182
+ standard (1.54.0)
163
183
  language_server-protocol (~> 3.17.0.2)
164
184
  lint_roller (~> 1.0)
165
- rubocop (~> 1.50.2)
185
+ rubocop (~> 1.84.0)
166
186
  standard-custom (~> 1.0.0)
167
- standard-performance (~> 1.0.1)
187
+ standard-performance (~> 1.8)
168
188
  standard-custom (1.0.2)
169
189
  lint_roller (~> 1.0)
170
190
  rubocop (~> 1.50)
171
- standard-performance (1.0.1)
172
- lint_roller (~> 1.0)
173
- rubocop-performance (~> 1.16.0)
191
+ standard-performance (1.9.0)
192
+ lint_roller (~> 1.1)
193
+ rubocop-performance (~> 1.26.0)
174
194
  standardrb (1.0.1)
175
195
  standard
176
- timecop (0.9.8)
177
- unicode-display_width (2.5.0)
178
- vcr (6.1.0)
196
+ timecop (0.9.10)
197
+ unicode-display_width (3.2.0)
198
+ unicode-emoji (~> 4.1)
199
+ unicode-emoji (4.2.0)
200
+ vcr (6.4.0)
179
201
  wasabi (3.7.0)
180
202
  addressable
181
203
  httpi (~> 2.0)
182
204
  nokogiri (>= 1.4.2)
183
- webmock (3.19.1)
205
+ webmock (3.26.1)
184
206
  addressable (>= 2.8.0)
185
207
  crack (>= 0.3.2)
186
208
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -188,9 +210,10 @@ GEM
188
210
  webrobots (0.1.2)
189
211
  xml-simple (1.1.9)
190
212
  rexml
191
- yard (0.9.36)
213
+ yard (0.9.38)
192
214
 
193
215
  PLATFORMS
216
+ arm64-darwin-25
194
217
  ruby
195
218
 
196
219
  DEPENDENCIES
@@ -211,4 +234,4 @@ DEPENDENCIES
211
234
  webmock
212
235
 
213
236
  BUNDLED WITH
214
- 2.4.19
237
+ 2.5.22
data/History.txt CHANGED
@@ -1,3 +1,10 @@
1
+ === 3.0.6 / 06.05.2026
2
+ * FHIR: extract Indikationscode (XXXXX.NN) from ClinicalUseDefinition resources by combining FOPHDossierNumber with each CUD's .NN id-suffix; expose as item[:indication_codes] (and per package) so downstream builders can include it on prescriptions and invoices as required by BAG from 2026-07-01 (issue #113)
3
+ * Declare csv as a runtime dependency in the gemspec (csv was removed from Ruby's default gems in 3.4)
4
+
5
+ === 3.0.5 / 25.04.2026
6
+ * Refdata cleanup: compensate the doubled-dose template bug in Refdata.Articles.xml (e.g. "30 mg / 30 mg / 100 Tablette") by collapsing to a single token. Guarded by a Swissmedic-side comma check so real combination products are untouched (issue #112)
7
+
1
8
  === 3.0.4 / 24.04.2026
2
9
  * Firstbase: switch -b/--firstbase from the deprecated pillbox.oddb.org XLSX to the GS1 Switzerland CSV at https://id.gs1.ch/01/07612345000961 (full firstbase barcode registry, ~189k items). Downloaded file is now firstbase.csv; FirstbaseExtractor now parses CSV with headers instead of XLSX.
3
10
 
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.4
54
+ /opt/src/oddb2xml/bin/oddb2xml version 3.0.5
55
55
  Usage:
56
56
  oddb2xml [option]
57
57
  produced files are found under data
@@ -112,7 +112,7 @@ FR
112
112
 
113
113
  ## Supported ruby version
114
114
 
115
- You will need ruby >= 2.5 to work correctly. Current development happens on Ruby 3.2 (`.ruby-version`).
115
+ You will need ruby >= 2.5 to work correctly. Current development happens on Ruby 3.3 (`.ruby-version`).
116
116
  CI runs on Ruby 3.0, 3.1 and 3.2 via GitHub Actions — see the badge above for the latest spec results.
117
117
 
118
118
 
@@ -292,6 +292,27 @@ We use the following files:
292
292
  * https://epl.bag.admin.ch/static/fhir/foph-sl-export-latest-{de,fr,it}.ndjson (FHIR NDJSON, used with `--fhir`)
293
293
  * https://id.gs1.ch/01/07612345000961 (GS1 Switzerland firstbase CSV — full barcode registry, used with `-b`/`--firstbase`)
294
294
 
295
+ ## Refdata data-quality compensation
296
+
297
+ Refdata.Articles.xml from `files.refdata.ch` ships with a number of recurring
298
+ data-quality issues that propagate into downstream systems unchanged. oddb2xml
299
+ applies a small set of conservative cleanups before emitting any output. See
300
+ GitHub issue [#112](https://github.com/zdavatz/oddb2xml/issues/112) for the
301
+ full catalogue and the parallel report sent to Refdata.
302
+
303
+ Currently active fixes (`lib/oddb2xml/refdata_cleanup.rb`):
304
+
305
+ * **Doubled dose token** — Refdata sometimes emits the strength twice in
306
+ `<FullName>`, e.g. `MIRTAZAPIN Sandoz eco 30 mg / 30 mg / 100 Tablette`.
307
+ When the matching Swissmedic entry shows a single active substance, the
308
+ duplicate token is collapsed to a single occurrence. Real combination
309
+ products (e.g. PHESGO 600 mg / 600 mg / 10 ml — pertuzumab + trastuzumab)
310
+ are detected via the comma in `substance_swissmedic` and left untouched.
311
+
312
+ The cleanup runs at the start of `prepare_articles` in `Builder` and is
313
+ idempotent. Each rule is guarded by a Swissmedic-side heuristic so genuine
314
+ data is never altered.
315
+
295
316
  ## Rules for matching GTIN (aka EAN13), product number and IKSNR
296
317
 
297
318
  For drugs which appear in Packungen.xlsx file published by Swissmedic the following rule is used to create the GTIN
@@ -88,12 +88,42 @@ module Oddb2xml
88
88
  end
89
89
  end
90
90
 
91
+ # Mutates @refdata in place to compensate for known Refdata.Articles.xml
92
+ # data-quality issues (see GitHub issue #112). Idempotent: subsequent
93
+ # calls are no-ops within the same Builder instance.
94
+ def apply_refdata_description_cleanups!
95
+ return if @refdata_descriptions_cleaned
96
+ @refdata_descriptions_cleaned = true
97
+ return if @refdata.nil? || @refdata.empty?
98
+ double_dose_fixed = 0
99
+ @refdata.each_value do |item|
100
+ next unless item.is_a?(Hash)
101
+ no8 = item[:no8]
102
+ next if no8.nil? || no8.empty?
103
+ pack = @packs[no8]
104
+ next unless pack
105
+ substance = pack[:substance_swissmedic]
106
+ [:desc_de, :desc_fr, :desc_it].each do |key|
107
+ original = item[key]
108
+ cleaned = RefdataCleanup.fix_double_dose(original, substance)
109
+ if cleaned != original
110
+ item[key] = cleaned
111
+ double_dose_fixed += 1
112
+ end
113
+ end
114
+ end
115
+ if double_dose_fixed > 0
116
+ Oddb2xml.log("Refdata cleanup: fixed double-dose pattern in #{double_dose_fixed} description(s)")
117
+ end
118
+ end
119
+
91
120
  private_class_method
92
121
 
93
122
  def prepare_articles(reset = false)
94
123
  @articles = nil if reset
95
124
  unless @articles
96
125
  Oddb2xml.log("prepare_articles starting with #{@articles ? @articles.size : "no"} articles.")
126
+ apply_refdata_description_cleanups!
97
127
  @articles = []
98
128
  @refdata.each do |ean13, obj|
99
129
  unless SKIP_MIGEL_DOWNLOADER
@@ -159,7 +159,7 @@ module Oddb2xml
159
159
  module FHIR
160
160
  # Bundle represents one line in the NDJSON file
161
161
  class Bundle
162
- attr_reader :medicinal_product, :packages, :authorizations, :ingredients
162
+ attr_reader :medicinal_product, :packages, :authorizations, :ingredients, :clinical_use_definitions
163
163
 
164
164
  def initialize(json_line)
165
165
  data = JSON.parse(json_line)
@@ -174,6 +174,7 @@ module Oddb2xml
174
174
  @packages = []
175
175
  @authorizations = []
176
176
  @ingredients = []
177
+ @clinical_use_definitions = []
177
178
 
178
179
  @entries.each do |entry|
179
180
  resource = entry["resource"]
@@ -186,11 +187,32 @@ module Oddb2xml
186
187
  @authorizations << Authorization.new(resource)
187
188
  when "Ingredient"
188
189
  @ingredients << Ingredient.new(resource)
190
+ when "ClinicalUseDefinition"
191
+ @clinical_use_definitions << ClinicalUseDefinition.new(resource)
189
192
  end
190
193
  end
191
194
  end
192
195
  end
193
196
 
197
+ # ClinicalUseDefinition carries one indication. Its `id` ends in ".NN",
198
+ # the per-indication suffix that combines with the FOPHDossierNumber
199
+ # (XXXXX) on the reimbursement RegulatedAuthorization to form the
200
+ # Indikationscode XXXXX.NN required by BAG from 2026-07-01.
201
+ class ClinicalUseDefinition
202
+ attr_reader :id, :nn_suffix, :type, :text
203
+
204
+ def initialize(resource)
205
+ @id = resource["id"]
206
+ @type = resource["type"]
207
+ @nn_suffix = @id&.[](/\.(\d{2})\z/, 1)
208
+ @text = resource.dig("indication", "diseaseSymptomProcedure", "concept", "text")
209
+ end
210
+
211
+ def indication?
212
+ @type == "indication"
213
+ end
214
+ end
215
+
194
216
  class MedicinalProduct
195
217
  attr_reader :names, :atc_code, :classification, :it_codes
196
218
 
@@ -427,6 +449,11 @@ module Oddb2xml
427
449
  prep.OrgGenCode = map_org_gen_code(mp.classification)
428
450
  prep.ItCode = mp.it_code # Add IT code
429
451
 
452
+ # Indikationscodes (BAG: XXXXX.NN, mandatory on prescriptions/invoices
453
+ # from 2026-07-01). Build from FOPHDossierNumber (reimbursement auth)
454
+ # plus each ClinicalUseDefinition's .NN suffix. See issue #113.
455
+ prep.IndicationCodes = build_indication_codes(bundle)
456
+
430
457
  # Map packages
431
458
  prep.Packs = OpenStruct.new
432
459
  prep.Packs.Pack = bundle.packages.map do |pkg|
@@ -535,6 +562,21 @@ module Oddb2xml
535
562
  reimbursement&.cost_share
536
563
  end
537
564
 
565
+ def build_indication_codes(bundle)
566
+ reimbursement = bundle.authorizations.find(&:reimbursement_sl?)
567
+ dossier = reimbursement&.foph_dossier_no
568
+ return [] unless dossier && !bundle.clinical_use_definitions.empty?
569
+
570
+ bundle.clinical_use_definitions.each_with_object([]) do |cud, acc|
571
+ next unless cud.indication? && cud.nn_suffix
572
+ acc << OpenStruct.new(
573
+ code: "#{dossier}.#{cud.nn_suffix}",
574
+ cud_id: cud.id,
575
+ text: cud.text
576
+ )
577
+ end
578
+ end
579
+
538
580
  def map_org_gen_code(classification)
539
581
  return nil unless classification
540
582
 
@@ -616,6 +658,13 @@ module Oddb2xml
616
658
  item[:comment_it] = ""
617
659
  item[:it_code] = (itc = seq.ItCode) ? itc : "" # NOW available in FHIR!
618
660
 
661
+ # Indikationscodes (BAG XXXXX.NN, see issue #113). Each entry is a
662
+ # Hash with :code, :cud_id, :text — mandatory on rx/invoices from
663
+ # 2026-07-01.
664
+ item[:indication_codes] = Array(seq.IndicationCodes).map do |ic|
665
+ {code: ic.code, cud_id: ic.cud_id, text: ic.text}
666
+ end
667
+
619
668
  # Build substances array
620
669
  item[:substances] = []
621
670
  if seq.Substances && seq.Substances.Substance
@@ -673,7 +722,8 @@ module Oddb2xml
673
722
  sl_entry: true,
674
723
  swissmedic_category: (cat = pac.SwissmedicCategory) ? cat : "",
675
724
  swissmedic_number8: (num = pac.SwissmedicNo8) ? num : "",
676
- prices: {exf_price: exf, pub_price: pub}
725
+ prices: {exf_price: exf, pub_price: pub},
726
+ indication_codes: item[:indication_codes]
677
727
  }
678
728
 
679
729
  # Map limitations from FHIR
@@ -0,0 +1,34 @@
1
+ module Oddb2xml
2
+ # Compensates for known data-quality issues in upstream Refdata.Articles.xml
3
+ # before they reach the generated output. Each fix is opt-in and guarded by
4
+ # a heuristic against Swissmedic data so we never alter genuine combination
5
+ # products. See GitHub issue #112 for the catalogue of upstream problems.
6
+ module RefdataCleanup
7
+ DOSE_TOKEN = /\d+(?:[.,]\d+)?\s*(?:mg|µg|mcg|g|ml|UI|U\.I\.|IE|%)/i
8
+ # Matches "<dose> / <same dose> /" – the templating bug where Refdata
9
+ # repeats the strength once. The backreference \1 only matches when the
10
+ # exact same dose string appears twice, which keeps real combos
11
+ # (e.g. PHESGO 600 mg / 600 mg / 10 ml) safe – those are caught by the
12
+ # single_substance? guard, but the literal-match also acts as a backstop.
13
+ DOUBLE_DOSE_RE = /(#{DOSE_TOKEN})\s*\/\s*\1\s*\/\s*/
14
+
15
+ # A Swissmedic compositions cell like "mirtazapinum" indicates a mono
16
+ # product; "atovaquonum, proguanili hydrochloridum" or
17
+ # "pertuzumabum, trastuzumabum" indicates a real combination.
18
+ def self.single_substance?(swissmedic_substance)
19
+ return false if swissmedic_substance.nil?
20
+ str = swissmedic_substance.to_s.strip
21
+ return false if str.empty?
22
+ !str.include?(",")
23
+ end
24
+
25
+ # Removes the duplicated dose token in mono products. Returns the
26
+ # cleaned description, or the original string if no change applies.
27
+ def self.fix_double_dose(desc, swissmedic_substance)
28
+ return desc if desc.nil? || desc.empty?
29
+ return desc unless DOUBLE_DOSE_RE.match?(desc)
30
+ return desc unless single_substance?(swissmedic_substance)
31
+ desc.sub(DOUBLE_DOSE_RE, '\1 / ')
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,3 @@
1
1
  module Oddb2xml
2
- VERSION = "3.0.4"
2
+ VERSION = "3.0.6"
3
3
  end
data/lib/oddb2xml.rb CHANGED
@@ -6,6 +6,7 @@ require "oddb2xml/options"
6
6
  require "oddb2xml/downloader"
7
7
  require "oddb2xml/xml_definitions"
8
8
  require "oddb2xml/extractor"
9
+ require "oddb2xml/refdata_cleanup"
9
10
  require "oddb2xml/builder"
10
11
  require "oddb2xml/fhir_support"
11
12
  require "oddb2xml/cli"
data/oddb2xml.gemspec CHANGED
@@ -38,6 +38,7 @@ Gem::Specification.new do |spec|
38
38
  spec.add_dependency "htmlentities"
39
39
  spec.add_dependency "webrick", ">= 1.8.2"
40
40
  spec.add_dependency "rexml", ">= 3.3.9"
41
+ spec.add_dependency "csv" # bundled with Ruby <= 3.3, gem from 3.4 onwards
41
42
  spec.add_dependency "standardrb"
42
43
  spec.add_dependency "rack", ">= 3.1.20"
43
44
 
@@ -0,0 +1 @@
1
+ {"resourceType":"Bundle","id":"9a91dc6e-3410-4020-9375-b2cae207e1bf","meta":{"profile":["http://fhir.ch/ig/ch-epl/StructureDefinition/ch-idmp-bundle"]},"type":"collection","entry":[{"fullUrl":"http://fhir.epl.bag.admin.ch/MedicinalProductDefinition/de872be1-02ff-4b8e-8691-658099182eb7","resource":{"resourceType":"MedicinalProductDefinition","id":"de872be1-02ff-4b8e-8691-658099182eb7","meta":{"profile":["http://fhir.ch/ig/ch-epl/StructureDefinition/ch-idmp-medicinalproductdefinition"]},"text":{"status":"generated","div":"<div xmlns=\"http://www.w3.org/1999/xhtml\">Cyramza Inf Konz 100 mg/10 ml</div>"},"classification":[{"coding":[{"system":"http://www.whocc.no/atc","code":"L01FG02","display":"Ramucirumab"}]},{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-epl-foph-index-therapeuticus","code":"070000","display":"07. STOFFWECHSEL"}]},{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-epl-foph-index-therapeuticus","code":"071600","display":"07.16. Oncologica"}]},{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-epl-foph-index-therapeuticus","code":"071610","display":"07.16.10. Cytostatica"}]}],"name":[{"productName":"Cyramza Inf Konz 100 mg/10 ml","usage":[{"country":{"coding":[{"system":"urn:iso:std:iso:3166","code":"CH","display":"Switzerland"}]},"language":{"coding":[{"system":"urn:ietf:bcp:47","code":"de-CH","display":"German (Switzerland)"}]}}]},{"productName":"Cyramza conc perf 100 mg/10 ml","usage":[{"country":{"coding":[{"system":"urn:iso:std:iso:3166","code":"CH","display":"Switzerland"}]},"language":{"coding":[{"system":"urn:ietf:bcp:47","code":"fr-CH","display":"French (Switzerland)"}]}}]},{"productName":"Cyramza Inf Konz 100 mg/10 ml","usage":[{"country":{"coding":[{"system":"urn:iso:std:iso:3166","code":"CH","display":"Switzerland"}]},"language":{"coding":[{"system":"urn:ietf:bcp:47","code":"it-CH","display":"Italian (Switzerland)"}]}}]}]}},{"fullUrl":"http://fhir.epl.bag.admin.ch/RegulatedAuthorization/65206","resource":{"resourceType":"RegulatedAuthorization","id":"65206","meta":{"profile":["http://fhir.ch/ig/ch-epl/StructureDefinition/ch-idmp-regulatedauthorization"]},"text":{"status":"generated","div":"<div xmlns=\"http://www.w3.org/1999/xhtml\">Marketing Authorisation 65206 for MedicinalProductDefinition de872be1-02ff-4b8e-8691-658099182eb7</div>"},"contained":[{"resourceType":"Organization","id":"7601001261853","meta":{"profile":["http://fhir.ch/ig/ch-epl/StructureDefinition/ch-idmp-organization"]},"identifier":[{"system":"urn:oid:2.51.1.3","value":"7601001261853"}],"name":"Eli Lilly (Suisse) SA"}],"identifier":[{"system":"http://fhir.ch/ig/ch-epl/sid/authno","value":"65206"}],"subject":[{"reference":"CHIDMPMedicinalProductDefinition/de872be1-02ff-4b8e-8691-658099182eb7"}],"type":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-authorisation-type","code":"756000002001","display":"Marketing Authorisation"}]},"region":[{"coding":[{"system":"urn:iso:std:iso:3166","code":"CH","display":"Switzerland"}]}],"holder":{"reference":"#7601001261853"}}},{"fullUrl":"http://fhir.epl.bag.admin.ch/PackagedProductDefinition/c3a247c9-d686-49ab-9344-892d8b93f148","resource":{"resourceType":"PackagedProductDefinition","id":"c3a247c9-d686-49ab-9344-892d8b93f148","meta":{"profile":["http://fhir.ch/ig/ch-epl/StructureDefinition/ch-idmp-packagedproductdefinition"]},"text":{"status":"generated","div":"<div xmlns=\"http://www.w3.org/1999/xhtml\">Cyramza Inf Konz 100 mg/10 ml Durchstf 1 Stk</div>"},"packageFor":[{"reference":"CHIDMPMedicinalProductDefinition/de872be1-02ff-4b8e-8691-658099182eb7"}],"containedItemQuantity":[{"unit":"Durchstf 1 Stk"}],"description":"Cyramza Inf Konz 100 mg/10 ml Durchstf 1 Stk","legalStatusOfSupply":[{"code":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-SMC-legal-status-of-supply","code":"756005022001","display":"Medicinal product subject to medical or veterinary prescription single dispensation (A)"}]}}],"packaging":{"identifier":[{"system":"urn:oid:2.51.1.1","value":"7680652060015"}]}}},{"fullUrl":"http://fhir.epl.bag.admin.ch/RegulatedAuthorization/65206001-c3a247c9-d686-49ab-9344-892d8b93f148","resource":{"resourceType":"RegulatedAuthorization","id":"65206001-c3a247c9-d686-49ab-9344-892d8b93f148","meta":{"profile":["http://fhir.ch/ig/ch-epl/StructureDefinition/ch-idmp-regulatedauthorization"]},"text":{"status":"generated","div":"<div xmlns=\"http://www.w3.org/1999/xhtml\">Marketing Authorisation 65206001-c3a247c9-d686-49ab-9344-892d8b93f148 for PackagedProductDefinition c3a247c9-d686-49ab-9344-892d8b93f148</div>"},"contained":[{"resourceType":"Organization","id":"7601001261853","meta":{"profile":["http://fhir.ch/ig/ch-epl/StructureDefinition/ch-idmp-organization"]},"identifier":[{"system":"urn:oid:2.51.1.3","value":"7601001261853"}],"name":"Eli Lilly (Suisse) SA"}],"identifier":[{"system":"http://fhir.ch/ig/ch-epl/sid/authno","value":"65206001"}],"subject":[{"reference":"CHIDMPPackagedProductDefinition/c3a247c9-d686-49ab-9344-892d8b93f148"}],"type":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-authorisation-type","code":"756000002001","display":"Marketing Authorisation"}]},"region":[{"coding":[{"system":"urn:iso:std:iso:3166","code":"CH","display":"Switzerland"}]}],"holder":{"reference":"#7601001261853"}}},{"fullUrl":"http://fhir.epl.bag.admin.ch/RegulatedAuthorization/9c50920a-2d9d-476f-89d8-9477c8840de9","resource":{"resourceType":"RegulatedAuthorization","id":"9c50920a-2d9d-476f-89d8-9477c8840de9","meta":{"profile":["http://fhir.ch/ig/ch-epl/StructureDefinition/ch-idmp-regulatedauthorization"]},"text":{"status":"generated","div":"<div xmlns=\"http://www.w3.org/1999/xhtml\">Reimbursement SL 9c50920a-2d9d-476f-89d8-9477c8840de9 for PackagedProductDefinition c3a247c9-d686-49ab-9344-892d8b93f148</div>"},"contained":[{"resourceType":"Organization","id":"7601001261853","meta":{"profile":["http://fhir.ch/ig/ch-epl/StructureDefinition/ch-idmp-organization"]},"identifier":[{"system":"urn:oid:2.51.1.3","value":"7601001261853"}],"name":"Eli Lilly (Suisse) SA"}],"extension":[{"url":"http://fhir.ch/ig/ch-epl/StructureDefinition/reimbursementSL","extension":[{"url":"FOPHDossierNumber","valueIdentifier":{"system":"urn:oid:2.16.756.1","value":"20403"}},{"url":"status","valueCodeableConcept":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-epl-foph-reimbursement-status","code":"756001021001","display":"Reimbursed"}]}},{"url":"statusDate","valueDate":"2019-06-01"},{"url":"listingStatus","valueCodeableConcept":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-epl-foph-listing-status","code":"756001002001","display":"Listed"}]}},{"url":"listingPeriod","valuePeriod":{"start":"2019-06-01"}},{"url":"firstListingDate","valueDate":"2016-03-01"},{"url":"costShare","valueInteger":10},{"url":"priceModel","valueBoolean":true},{"url":"http://fhir.ch/ig/ch-epl/StructureDefinition/productPrice","extension":[{"url":"type","valueCodeableConcept":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-epl-foph-price-type","code":"756002005001","display":"Retail price"}]}},{"url":"changeType","valueCodeableConcept":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-epl-foph-type-of-price-change","code":"756002006009","display":"Normal price mutation"}]}},{"url":"value","valueMoney":{"value":421.65,"currency":"CHF"}},{"url":"changeDate","valueDate":"2024-10-01"}]},{"url":"http://fhir.ch/ig/ch-epl/StructureDefinition/productPrice","extension":[{"url":"type","valueCodeableConcept":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-epl-foph-price-type","code":"756002005002","display":"Ex-factory price"}]}},{"url":"changeType","valueCodeableConcept":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-epl-foph-type-of-price-change","code":"756002006009","display":"Normal price mutation"}]}},{"url":"value","valueMoney":{"value":372.61,"currency":"CHF"}},{"url":"changeDate","valueDate":"2024-10-01"}]}]}],"subject":[{"reference":"CHIDMPPackagedProductDefinition/c3a247c9-d686-49ab-9344-892d8b93f148"}],"type":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-authorisation-type","code":"756000002003","display":"Reimbursement SL"}]},"indication":[{"extension":[{"url":"http://fhir.ch/ig/ch-epl/StructureDefinition/regulatedAuthorization-limitation","extension":[{"url":"status","valueCodeableConcept":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-epl-foph-limitationstatus","code":"756002071001","display":"Limitation Reimbursed"}]}},{"url":"statusDate","valueDate":"2024-10-01"},{"url":"period","valuePeriod":{"start":"2024-10-01"}},{"url":"firstLimitationDate","valueDate":"2016-03-01"},{"url":"indicationCode","valueString":"20403.01"},{"url":"limitationIndication","valueReference":{"reference":"ClinicalUseDefinition/CYRAMZA.01"}}]}]},{"extension":[{"url":"http://fhir.ch/ig/ch-epl/StructureDefinition/regulatedAuthorization-limitation","extension":[{"url":"status","valueCodeableConcept":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-epl-foph-limitationstatus","code":"756002071001","display":"Limitation Reimbursed"}]}},{"url":"statusDate","valueDate":"2024-10-01"},{"url":"period","valuePeriod":{"start":"2024-10-01","end":"2027-09-30"}},{"url":"reimbursementEndDate","valueDate":"2027-12-31"},{"url":"firstLimitationDate","valueDate":"2016-03-01"},{"url":"indicationCode","valueString":"20403.02"},{"url":"limitationIndication","valueReference":{"reference":"ClinicalUseDefinition/CYRAMZA.02"}}]}]}],"holder":{"reference":"#7601001261853"}}},{"fullUrl":"http://fhir.epl.bag.admin.ch/Ingredient/a5edbabc-f9d6-4019-ac6f-2a80fe033339","resource":{"resourceType":"Ingredient","id":"a5edbabc-f9d6-4019-ac6f-2a80fe033339","meta":{"profile":["http://fhir.ch/ig/ch-epl/StructureDefinition/ch-idmp-ingredient"]},"text":{"status":"generated","div":"<div xmlns=\"http://www.w3.org/1999/xhtml\">Ramucirumabum 100 mg</div>"},"status":"draft","for":[{"reference":"CHIDMPMedicinalProductDefinition/de872be1-02ff-4b8e-8691-658099182eb7"}],"role":{"coding":[{"system":"http://fhir.ch/ig/ch-epl/CodeSystem/ch-SMC-ingredient-role","code":"756005051001","display":"Active"}]},"substance":{"code":{"concept":{"text":"Ramucirumabum"}},"strength":[{"presentationQuantity":{"value":100,"unit":"mg"}}]}}},{"fullUrl":"http://fhir.epl.bag.admin.ch/ClinicalUseDefinition/CYRAMZA.01","resource":{"resourceType":"ClinicalUseDefinition","id":"CYRAMZA.01","text":{"status":"generated","div":"<div xmlns=\"http://www.w3.org/1999/xhtml\">In Kombination mit Paclitaxel für die Behandlung von erwachsenen Patienten mit ECOG-Status 0 oder 1 mit fortgeschrittenem Adenokarzinom des Magens oder gastroösophagealen Übergangs mit einem Progress nach vorausgegangener Platin- und Fluoropyrimidin-haltiger Chemotherapie.\nAls Monotherapie für die Behandlung von erwachsenen Patienten mit ECOG-Status 0 oder 1 mit fortgeschrittenem Adenokarzinom des Magens oder des gastroösophagealen Übergangs mit einem Progress nach vorausgegangener Platin- oder Fluoropyrimidin-haltiger Chemotherapie, wenn diese Patienten für eine Kombinationstherapie mit Paclitaxel nicht geeignet sind.\nDie Behandlung bedarf der Kostengutsprache durch den Krankenversicherer nach vorgängiger Konsultation des Vertrauensarztes.\nFolgender Indikationscode ist an den Krankenversicherer zu übermitteln: 20403.01</div>"},"type":"indication","indication":{"diseaseSymptomProcedure":{"concept":{"text":"In Kombination mit Paclitaxel für die Behandlung von erwachsenen Patienten mit ECOG-Status 0 oder 1 mit fortgeschrittenem Adenokarzinom des Magens oder gastroösophagealen Übergangs mit einem Progress nach vorausgegangener Platin- und Fluoropyrimidin-haltiger Chemotherapie.\nAls Monotherapie für die Behandlung von erwachsenen Patienten mit ECOG-Status 0 oder 1 mit fortgeschrittenem Adenokarzinom des Magens oder des gastroösophagealen Übergangs mit einem Progress nach vorausgegangener Platin- oder Fluoropyrimidin-haltiger Chemotherapie, wenn diese Patienten für eine Kombinationstherapie mit Paclitaxel nicht geeignet sind.\nDie Behandlung bedarf der Kostengutsprache durch den Krankenversicherer nach vorgängiger Konsultation des Vertrauensarztes.\nFolgender Indikationscode ist an den Krankenversicherer zu übermitteln: 20403.01"}}}}},{"fullUrl":"http://fhir.epl.bag.admin.ch/ClinicalUseDefinition/CYRAMZA.02","resource":{"resourceType":"ClinicalUseDefinition","id":"CYRAMZA.02","text":{"status":"generated","div":"<div xmlns=\"http://www.w3.org/1999/xhtml\">In Kombination mit FOLFIRI (Irinotecan, Folsäure und 5-Fluorouracil) zur Behandlung von erwachsenen Patienten mit metastasiertem kolorektalem Karzinom (mKRK) mit Progress während oder nach vorausgegangener Therapie mit Bevacizumab, Oxaliplatin und einem Fluoropyrimidin. Die Behandlung bedarf der Kostengutsprache durch den Krankenversicherer nach vorgängiger Konsultation des Vertrauensarztes.\nDie Eli Lilly (Suisse) SA vergütet dem Krankenversicherer, bei dem die versicherte Person zum Zeitpunkt des Bezugs versichert war, auf dessen erste Aufforderung hin für auf jede bezogene Packung CYRAMZA einen festgelegten Anteil des Fabrikabgabepreises zurück. Sie gibt dem Krankenversicherer die Höhe der Rückvergütung bekannt. Die Mehrwertsteuer kann nicht zusätzlich zu diesem Anteil des Fabrikabgabepreises zurückgefordert werden. Die Aufforderung zur Rückvergütung soll ab dem Zeitpunkt der Verabreichung erfolgen.\nFolgender Indikationscode ist an den Krankenversicherer zu übermitteln: 20403.02</div>"},"type":"indication","indication":{"diseaseSymptomProcedure":{"concept":{"text":"In Kombination mit FOLFIRI (Irinotecan, Folsäure und 5-Fluorouracil) zur Behandlung von erwachsenen Patienten mit metastasiertem kolorektalem Karzinom (mKRK) mit Progress während oder nach vorausgegangener Therapie mit Bevacizumab, Oxaliplatin und einem Fluoropyrimidin. Die Behandlung bedarf der Kostengutsprache durch den Krankenversicherer nach vorgängiger Konsultation des Vertrauensarztes.\nDie Eli Lilly (Suisse) SA vergütet dem Krankenversicherer, bei dem die versicherte Person zum Zeitpunkt des Bezugs versichert war, auf dessen erste Aufforderung hin für auf jede bezogene Packung CYRAMZA einen festgelegten Anteil des Fabrikabgabepreises zurück. Sie gibt dem Krankenversicherer die Höhe der Rückvergütung bekannt. Die Mehrwertsteuer kann nicht zusätzlich zu diesem Anteil des Fabrikabgabepreises zurückgefordert werden. Die Aufforderung zur Rückvergütung soll ab dem Zeitpunkt der Verabreichung erfolgen.\nFolgender Indikationscode ist an den Krankenversicherer zu übermitteln: 20403.02"}}}}}]}
data/spec/fhir_spec.rb ADDED
@@ -0,0 +1,53 @@
1
+ require "spec_helper"
2
+ require "oddb2xml/downloader"
3
+ require "oddb2xml/extractor"
4
+ require "oddb2xml/fhir_support"
5
+
6
+ describe "FHIR Indikationscode support" do
7
+ let(:cyramza_fixture) { File.join(Oddb2xml::SpecData, "fhir", "cyramza.ndjson") }
8
+
9
+ describe Oddb2xml::FHIR::ClinicalUseDefinition do
10
+ it "extracts the .NN suffix from id" do
11
+ cud = described_class.new("id" => "CYRAMZA.02", "type" => "indication")
12
+ expect(cud.nn_suffix).to eq("02")
13
+ expect(cud.indication?).to be true
14
+ end
15
+
16
+ it "returns nil suffix for non-conforming ids" do
17
+ cud = described_class.new("id" => "CYRAMZA")
18
+ expect(cud.nn_suffix).to be_nil
19
+ end
20
+ end
21
+
22
+ describe Oddb2xml::FHIR::Bundle do
23
+ it "collects ClinicalUseDefinition entries" do
24
+ line = File.read(cyramza_fixture).lines.first
25
+ bundle = described_class.new(line)
26
+ expect(bundle.clinical_use_definitions).not_to be_empty
27
+ expect(bundle.clinical_use_definitions.map(&:id)).to include("CYRAMZA.01", "CYRAMZA.02")
28
+ end
29
+ end
30
+
31
+ describe Oddb2xml::FHIR::PreparationsParser do
32
+ it "constructs XXXXX.NN indication codes from FOPHDossierNumber + CUD suffix" do
33
+ parser = described_class.new(cyramza_fixture)
34
+ prep = parser.preparations.first
35
+ codes = prep.IndicationCodes.map(&:code)
36
+ expect(codes).to include("20403.01", "20403.02")
37
+ end
38
+ end
39
+
40
+ describe Oddb2xml::FhirExtractor do
41
+ it "exposes indication_codes on each item and package" do
42
+ data = described_class.new(cyramza_fixture).to_hash
43
+ expect(data).not_to be_empty
44
+
45
+ item = data.values.first
46
+ codes = item[:indication_codes].map { |ic| ic[:code] }
47
+ expect(codes).to include("20403.01", "20403.02")
48
+
49
+ pkg = item[:packages].values.first
50
+ expect(pkg[:indication_codes]).to eq(item[:indication_codes])
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,151 @@
1
+ require "spec_helper"
2
+ require "oddb2xml/refdata_cleanup"
3
+
4
+ describe Oddb2xml::RefdataCleanup do
5
+ describe ".single_substance?" do
6
+ it "returns true for a single Swissmedic substance" do
7
+ expect(described_class.single_substance?("mirtazapinum")).to be true
8
+ expect(described_class.single_substance?("methotrexatum")).to be true
9
+ end
10
+
11
+ it "returns false when multiple substances are listed (combo)" do
12
+ expect(described_class.single_substance?("pertuzumabum, trastuzumabum")).to be false
13
+ expect(described_class.single_substance?("atovaquonum, proguanili hydrochloridum")).to be false
14
+ end
15
+
16
+ it "returns false when input is nil or empty" do
17
+ expect(described_class.single_substance?(nil)).to be false
18
+ expect(described_class.single_substance?("")).to be false
19
+ expect(described_class.single_substance?(" ")).to be false
20
+ end
21
+ end
22
+
23
+ describe ".fix_double_dose" do
24
+ let(:mono) { "mirtazapinum" }
25
+ let(:combo) { "pertuzumabum, trastuzumabum" }
26
+
27
+ it "removes the duplicate dose for a mono product" do
28
+ input = "MIRTAZAPIN Sandoz eco 30 mg / 30 mg / 100 Tablette"
29
+ expected = "MIRTAZAPIN Sandoz eco 30 mg / 100 Tablette"
30
+ expect(described_class.fix_double_dose(input, mono)).to eq expected
31
+ end
32
+
33
+ it "handles ICATIBANT-style spacing" do
34
+ input = "ICATIBANT Spirig HC 30 mg / 30 mg / 1 x 3 ml"
35
+ expected = "ICATIBANT Spirig HC 30 mg / 1 x 3 ml"
36
+ expect(described_class.fix_double_dose(input, mono)).to eq expected
37
+ end
38
+
39
+ it "leaves real combinations untouched (PHESGO 600 mg / 600 mg / 10 ml)" do
40
+ input = "PHESGO Inj Lös 600 mg/600 mg/10 ml Durchstf"
41
+ expect(described_class.fix_double_dose(input, combo)).to eq input
42
+ end
43
+
44
+ it "leaves descriptions without the double-dose pattern untouched" do
45
+ input = "LEVOCETIRIZIN Spirig HC Filmtabl 5 mg 10 Stk"
46
+ expect(described_class.fix_double_dose(input, mono)).to eq input
47
+ end
48
+
49
+ it "leaves the description untouched when Swissmedic substance is unknown" do
50
+ input = "MIRTAZAPIN Sandoz eco 30 mg / 30 mg / 100 Tablette"
51
+ expect(described_class.fix_double_dose(input, nil)).to eq input
52
+ expect(described_class.fix_double_dose(input, "")).to eq input
53
+ end
54
+
55
+ it "is a no-op for nil or empty descriptions" do
56
+ expect(described_class.fix_double_dose(nil, mono)).to be_nil
57
+ expect(described_class.fix_double_dose("", mono)).to eq ""
58
+ end
59
+
60
+ it "does not collapse different doses (X mg / Y mg)" do
61
+ input = "FOO 250 mg / 100 mg / 12 Stk"
62
+ expect(described_class.fix_double_dose(input, combo)).to eq input
63
+ end
64
+ end
65
+ end
66
+
67
+ describe Oddb2xml::Builder do
68
+ describe "#apply_refdata_description_cleanups!" do
69
+ let(:builder) { Oddb2xml::Builder.new }
70
+
71
+ it "fixes double-dose entries on mono products" do
72
+ builder.packs = {
73
+ "69475006" => {substance_swissmedic: "mirtazapinum"}
74
+ }
75
+ builder.refdata = {
76
+ "7680694750066" => {
77
+ ean13: "7680694750066",
78
+ no8: "69475006",
79
+ desc_de: "MIRTAZAPIN Sandoz eco 30 mg / 30 mg / 100 Tablette",
80
+ desc_fr: "MIRTAZAPIN Sandoz eco 30 mg / 30 mg / 100 comprimé(",
81
+ desc_it: ""
82
+ }
83
+ }
84
+
85
+ builder.apply_refdata_description_cleanups!
86
+
87
+ item = builder.refdata["7680694750066"]
88
+ expect(item[:desc_de]).to eq "MIRTAZAPIN Sandoz eco 30 mg / 100 Tablette"
89
+ expect(item[:desc_fr]).to eq "MIRTAZAPIN Sandoz eco 30 mg / 100 comprimé("
90
+ end
91
+
92
+ it "leaves combo products untouched" do
93
+ builder.packs = {
94
+ "67828001" => {substance_swissmedic: "pertuzumabum, trastuzumabum"}
95
+ }
96
+ original = "PHESGO Inj Lös 600 mg/600 mg/10 ml Durchstf"
97
+ builder.refdata = {
98
+ "7680678280013" => {
99
+ ean13: "7680678280013",
100
+ no8: "67828001",
101
+ desc_de: original,
102
+ desc_fr: "",
103
+ desc_it: ""
104
+ }
105
+ }
106
+
107
+ builder.apply_refdata_description_cleanups!
108
+
109
+ expect(builder.refdata["7680678280013"][:desc_de]).to eq original
110
+ end
111
+
112
+ it "is idempotent" do
113
+ builder.packs = {
114
+ "69475006" => {substance_swissmedic: "mirtazapinum"}
115
+ }
116
+ builder.refdata = {
117
+ "7680694750066" => {
118
+ ean13: "7680694750066",
119
+ no8: "69475006",
120
+ desc_de: "MIRTAZAPIN Sandoz eco 30 mg / 30 mg / 100 Tablette",
121
+ desc_fr: "",
122
+ desc_it: ""
123
+ }
124
+ }
125
+
126
+ builder.apply_refdata_description_cleanups!
127
+ builder.apply_refdata_description_cleanups!
128
+
129
+ expect(builder.refdata["7680694750066"][:desc_de])
130
+ .to eq "MIRTAZAPIN Sandoz eco 30 mg / 100 Tablette"
131
+ end
132
+
133
+ it "skips entries without a Swissmedic match" do
134
+ builder.packs = {}
135
+ input = "MIRTAZAPIN Sandoz eco 30 mg / 30 mg / 100 Tablette"
136
+ builder.refdata = {
137
+ "7680694750066" => {
138
+ ean13: "7680694750066",
139
+ no8: "69475006",
140
+ desc_de: input,
141
+ desc_fr: "",
142
+ desc_it: ""
143
+ }
144
+ }
145
+
146
+ builder.apply_refdata_description_cleanups!
147
+
148
+ expect(builder.refdata["7680694750066"][:desc_de]).to eq input
149
+ end
150
+ end
151
+ 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.4
4
+ version: 3.0.6
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-04-24 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
@@ -262,6 +261,20 @@ dependencies:
262
261
  - - ">="
263
262
  - !ruby/object:Gem::Version
264
263
  version: 3.3.9
264
+ - !ruby/object:Gem::Dependency
265
+ name: csv
266
+ requirement: !ruby/object:Gem::Requirement
267
+ requirements:
268
+ - - ">="
269
+ - !ruby/object:Gem::Version
270
+ version: '0'
271
+ type: :runtime
272
+ prerelease: false
273
+ version_requirements: !ruby/object:Gem::Requirement
274
+ requirements:
275
+ - - ">="
276
+ - !ruby/object:Gem::Version
277
+ version: '0'
265
278
  - !ruby/object:Gem::Dependency
266
279
  name: standardrb
267
280
  requirement: !ruby/object:Gem::Requirement
@@ -470,6 +483,7 @@ files:
470
483
  - lib/oddb2xml/fhir_support.rb
471
484
  - lib/oddb2xml/options.rb
472
485
  - lib/oddb2xml/parslet_compositions.rb
486
+ - lib/oddb2xml/refdata_cleanup.rb
473
487
  - lib/oddb2xml/semantic_check.rb
474
488
  - lib/oddb2xml/util.rb
475
489
  - lib/oddb2xml/version.rb
@@ -512,6 +526,7 @@ files:
512
526
  - spec/data/compressor/oddb2xml_files_lppv.txt
513
527
  - spec/data/compressor/oddb2xml_files_nonpharma.xls
514
528
  - spec/data/epha_interactions.csv
529
+ - spec/data/fhir/cyramza.ndjson
515
530
  - spec/data/listen_neu.html
516
531
  - spec/data/medregbm_betrieb.txt
517
532
  - spec/data/medregbm_person.txt
@@ -539,11 +554,13 @@ files:
539
554
  - spec/data_helper.rb
540
555
  - spec/downloader_spec.rb
541
556
  - spec/extractor_spec.rb
557
+ - spec/fhir_spec.rb
542
558
  - spec/fixtures/vcr_cassettes/artikelstamm.json
543
559
  - spec/fixtures/vcr_cassettes/oddb2xml.json
544
560
  - spec/galenic_spec.rb
545
561
  - spec/options_spec.rb
546
562
  - spec/parslet_spec.rb
563
+ - spec/refdata_cleanup_spec.rb
547
564
  - spec/spec_helper.rb
548
565
  - test_options.rb
549
566
  - tools/cacert.pem
@@ -553,7 +570,6 @@ homepage: https://github.com/zdavatz/oddb2xml
553
570
  licenses:
554
571
  - GPL-3.0-only
555
572
  metadata: {}
556
- post_install_message:
557
573
  rdoc_options: []
558
574
  require_paths:
559
575
  - lib
@@ -568,8 +584,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
568
584
  - !ruby/object:Gem::Version
569
585
  version: '0'
570
586
  requirements: []
571
- rubygems_version: 3.5.22
572
- signing_key:
587
+ rubygems_version: 3.6.9
573
588
  specification_version: 4
574
589
  summary: oddb2xml creates xml files.
575
590
  test_files:
@@ -608,6 +623,7 @@ test_files:
608
623
  - spec/data/compressor/oddb2xml_files_lppv.txt
609
624
  - spec/data/compressor/oddb2xml_files_nonpharma.xls
610
625
  - spec/data/epha_interactions.csv
626
+ - spec/data/fhir/cyramza.ndjson
611
627
  - spec/data/listen_neu.html
612
628
  - spec/data/medregbm_betrieb.txt
613
629
  - spec/data/medregbm_person.txt
@@ -635,9 +651,11 @@ test_files:
635
651
  - spec/data_helper.rb
636
652
  - spec/downloader_spec.rb
637
653
  - spec/extractor_spec.rb
654
+ - spec/fhir_spec.rb
638
655
  - spec/fixtures/vcr_cassettes/artikelstamm.json
639
656
  - spec/fixtures/vcr_cassettes/oddb2xml.json
640
657
  - spec/galenic_spec.rb
641
658
  - spec/options_spec.rb
642
659
  - spec/parslet_spec.rb
660
+ - spec/refdata_cleanup_spec.rb
643
661
  - spec/spec_helper.rb