Skip to content

Commit bff42f3

Browse files
authored
Slot classes (#27514)
* Slot classes Slot classes * copilot suggestion copilot suggestion * implemented the two suggestions from PR review implemented the two suggestions from PR review, updated tests
1 parent 01edbda commit bff42f3

File tree

6 files changed

+280
-0
lines changed

6 files changed

+280
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
module VAOS
4+
module V2
5+
module Unified
6+
class BaseSlot
7+
attr_accessor :id, :start, :end, :provider_id, :provider_type
8+
9+
def initialize(attrs = {})
10+
attrs.each { |key, value| send(:"#{key}=", value) if respond_to?(:"#{key}=") }
11+
end
12+
end
13+
end
14+
end
15+
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
module VAOS
4+
module V2
5+
module Unified
6+
class EpsSlot < BaseSlot
7+
DURATION_SEGMENT_PATTERN = /\A(?:(?<hours>\d+)h)?(?:(?<minutes>\d+)m)?(?:(?<seconds>\d+)s)?\z/
8+
9+
attr_accessor :provider_service_id
10+
11+
def initialize(attrs = {})
12+
super
13+
self.provider_type = 'community_care'
14+
end
15+
16+
# Builds an EpsSlot from an EPS slot response (Hash).
17+
# EPS slots carry an opaque composite ID and a providerServiceId.
18+
# EPS slots may not include an explicit end time; when missing, we derive it from the
19+
# duration segment in the slot ID when possible, otherwise it is left nil.
20+
def self.from_eps_slot(slot)
21+
slot = slot.to_h if slot.is_a?(OpenStruct)
22+
23+
new(
24+
id: slot[:id],
25+
start: slot[:start],
26+
end: normalized_end_time(slot),
27+
provider_id: slot[:provider_service_id],
28+
provider_service_id: slot[:provider_service_id]
29+
)
30+
end
31+
32+
def self.normalized_end_time(slot)
33+
return slot[:end] if slot[:end].present?
34+
35+
duration = duration_from_slot_id(slot[:id])
36+
return nil if slot[:start].blank? || duration.nil?
37+
38+
start_time = Time.zone.parse(slot[:start].to_s)
39+
return nil if start_time.nil?
40+
41+
(start_time.utc + duration).iso8601
42+
end
43+
44+
def self.duration_from_slot_id(slot_id)
45+
duration_segment = slot_id.to_s.split('|')[3]
46+
return nil if duration_segment.blank?
47+
48+
match = DURATION_SEGMENT_PATTERN.match(duration_segment)
49+
return nil unless match
50+
51+
match[:hours].to_i.hours + match[:minutes].to_i.minutes + match[:seconds].to_i.seconds
52+
end
53+
end
54+
end
55+
end
56+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
3+
module VAOS
4+
module V2
5+
module Unified
6+
class VASlot < BaseSlot
7+
attr_accessor :location_id, :clinic_ien
8+
9+
def initialize(attrs = {})
10+
super
11+
self.provider_type = 'va'
12+
end
13+
14+
# Builds a VASlot from a VAOS slot response (OpenStruct or Hash).
15+
# VA slots carry an opaque encoded ID plus embedded location/clinic context.
16+
def self.from_vaos_slot(slot, location_id: nil)
17+
slot = slot.to_h if slot.is_a?(OpenStruct)
18+
19+
clinic_ien_value = slot.dig(:clinic, :clinic_ien)
20+
location_id_value = location_id || slot.dig(:location, :vha_facility_id)
21+
22+
new(
23+
id: slot[:id],
24+
start: slot[:start],
25+
end: slot[:end],
26+
provider_id: clinic_ien_value,
27+
location_id: location_id_value,
28+
clinic_ien: clinic_ien_value
29+
)
30+
end
31+
end
32+
end
33+
end
34+
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe VAOS::V2::Unified::BaseSlot do
6+
describe '#initialize' do
7+
it 'accepts attributes via hash' do
8+
slot = described_class.new(
9+
id: 'slot-123',
10+
start: '2025-01-02T11:00:00Z',
11+
end: '2025-01-02T11:30:00Z',
12+
provider_id: 'provider-456',
13+
provider_type: 'va'
14+
)
15+
16+
expect(slot.id).to eq('slot-123')
17+
expect(slot.start).to eq('2025-01-02T11:00:00Z')
18+
expect(slot.end).to eq('2025-01-02T11:30:00Z')
19+
expect(slot.provider_id).to eq('provider-456')
20+
expect(slot.provider_type).to eq('va')
21+
end
22+
23+
it 'defaults all attributes to nil' do
24+
slot = described_class.new
25+
26+
expect(slot.id).to be_nil
27+
expect(slot.start).to be_nil
28+
expect(slot.end).to be_nil
29+
expect(slot.provider_id).to be_nil
30+
expect(slot.provider_type).to be_nil
31+
end
32+
33+
it 'ignores unknown attributes' do
34+
slot = described_class.new(id: 'slot-123', unknown_field: 'ignored')
35+
36+
expect(slot.id).to eq('slot-123')
37+
expect(slot).not_to respond_to(:unknown_field)
38+
end
39+
end
40+
end
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe VAOS::V2::Unified::EpsSlot do
6+
describe '#initialize' do
7+
it 'sets provider_type to community_care' do
8+
slot = described_class.new
9+
expect(slot.provider_type).to eq('community_care')
10+
end
11+
end
12+
13+
describe '.from_eps_slot' do
14+
let(:eps_slot_id) do
15+
'5vuTac8v-practitioner-1-role-2|e43a19a8-b0cb-4dcf-befa-8cc511c3999b|' \
16+
'2025-01-02T11:00:00Z|30m0s|1736636444704|ov'
17+
end
18+
19+
let(:eps_slot) do
20+
{
21+
id: eps_slot_id,
22+
provider_service_id: '9mN718pH',
23+
start: '2025-01-02T11:00:00Z',
24+
end: nil
25+
}
26+
end
27+
28+
it 'maps EPS slot fields to EpsSlot' do
29+
slot = described_class.from_eps_slot(eps_slot)
30+
31+
expect(slot.id).to eq(eps_slot_id)
32+
expect(slot.start).to eq('2025-01-02T11:00:00Z')
33+
expect(slot.end).to eq('2025-01-02T11:30:00Z')
34+
expect(slot.provider_id).to eq('9mN718pH')
35+
expect(slot.provider_type).to eq('community_care')
36+
expect(slot.provider_service_id).to eq('9mN718pH')
37+
end
38+
39+
it 'works with OpenStruct input' do
40+
slot = described_class.from_eps_slot(OpenStruct.new(eps_slot))
41+
42+
expect(slot.id).to include('5vuTac8v-practitioner')
43+
expect(slot.provider_type).to eq('community_care')
44+
end
45+
46+
it 'handles missing provider_service_id gracefully' do
47+
eps_slot.delete(:provider_service_id)
48+
slot = described_class.from_eps_slot(eps_slot)
49+
50+
expect(slot.provider_id).to be_nil
51+
expect(slot.provider_service_id).to be_nil
52+
end
53+
54+
it 'returns nil end when slot ID has no parseable duration segment' do
55+
eps_slot[:id] = 'no-pipe-segments'
56+
eps_slot[:end] = nil
57+
slot = described_class.from_eps_slot(eps_slot)
58+
59+
expect(slot.end).to be_nil
60+
end
61+
62+
it 'preserves an explicit end time when present' do
63+
eps_slot[:end] = '2025-01-02T11:45:00Z'
64+
slot = described_class.from_eps_slot(eps_slot)
65+
66+
expect(slot.end).to eq('2025-01-02T11:45:00Z')
67+
end
68+
end
69+
end
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe VAOS::V2::Unified::VASlot do
6+
describe '#initialize' do
7+
it 'sets provider_type to va' do
8+
slot = described_class.new
9+
expect(slot.provider_type).to eq('va')
10+
end
11+
end
12+
13+
describe '.from_vaos_slot' do
14+
let(:vaos_slot) do
15+
{
16+
id: '3230323530313032323A323033303030303030303030',
17+
start: '2025-01-02T20:30:00Z',
18+
end: '2025-01-02T21:00:00Z',
19+
location: { vha_facility_id: '757GC', name: 'Marion VA Clinic' },
20+
clinic: { clinic_ien: '455', name: 'MARION CBOC PODIATRY' },
21+
practitioner: { name: 'Doe, John D, MD' }
22+
}
23+
end
24+
25+
it 'maps VAOS slot fields to VaSlot' do
26+
slot = described_class.from_vaos_slot(vaos_slot)
27+
28+
expect(slot.id).to eq('3230323530313032323A323033303030303030303030')
29+
expect(slot.start).to eq('2025-01-02T20:30:00Z')
30+
expect(slot.end).to eq('2025-01-02T21:00:00Z')
31+
expect(slot.provider_id).to eq('455')
32+
expect(slot.provider_type).to eq('va')
33+
expect(slot.location_id).to eq('757GC')
34+
expect(slot.clinic_ien).to eq('455')
35+
end
36+
37+
it 'allows location_id override via keyword argument' do
38+
slot = described_class.from_vaos_slot(vaos_slot, location_id: '983')
39+
40+
expect(slot.provider_id).to eq('455')
41+
expect(slot.location_id).to eq('983')
42+
end
43+
44+
it 'works with OpenStruct input' do
45+
slot = described_class.from_vaos_slot(OpenStruct.new(vaos_slot))
46+
47+
expect(slot.id).to eq('3230323530313032323A323033303030303030303030')
48+
expect(slot.provider_type).to eq('va')
49+
end
50+
51+
it 'handles missing location gracefully' do
52+
vaos_slot.delete(:location)
53+
slot = described_class.from_vaos_slot(vaos_slot)
54+
55+
expect(slot.provider_id).to eq('455')
56+
expect(slot.location_id).to be_nil
57+
end
58+
59+
it 'handles missing clinic gracefully' do
60+
vaos_slot.delete(:clinic)
61+
slot = described_class.from_vaos_slot(vaos_slot)
62+
63+
expect(slot.clinic_ien).to be_nil
64+
end
65+
end
66+
end

0 commit comments

Comments
 (0)