diff --git a/app/api/ica/endpoints/v1/transactions.rb b/app/api/ica/endpoints/v1/transactions.rb index 4a88cad..4bbc32e 100644 --- a/app/api/ica/endpoints/v1/transactions.rb +++ b/app/api/ica/endpoints/v1/transactions.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'rfid_tid' + module ICA::Endpoints::V1 # Receives information about new or updated parking transactions # rubocop:disable Metrics/ClassLength @@ -94,7 +96,8 @@ def carpark # To avoid problems with duplicate card numbers, we need to look up the concrete RFID tag belonging to the # media key specified in the request. def rfid_tag_information - rfid_tag_id = garage_system.card_account_mappings.find_by(card_key: params[:Media][:MediaKey]).rfid_tag_id + rfid_tag_id = garage_system.card_account_mappings.find_by(card_key: params[:Media][:MediaKey])&.rfid_tag_id || + RfidTag.find_by(uid: RfidTid.new(params[:DriveIn][:Media][:MediaId]).to_s).id { rfid_tag: { id: rfid_tag_id } } @@ -212,4 +215,5 @@ def finish_or_cancel_parking_transaction(facade_arguments) end end end + # rubocop:enable Metrics/ClassLength end diff --git a/lib/rfid_tid.rb b/lib/rfid_tid.rb new file mode 100644 index 0000000..a163909 --- /dev/null +++ b/lib/rfid_tid.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# Utility to work with RFID tag TIDs +class RfidTid + def initialize(tid) + @tid = sanitize(tid) + end + + def xtid? + e2? && bit_set?(8) + end + + def security? + e2? && bit_set?(9) + end + + def file? + e2? && bit_set?(10) + end + + def designer + bits[11, 9].to_i + end + + def model_number + bits[20, 12].to_i + end + + def to_s + @tid + end + + private + + def e2? + @tid.starts_with?('E2') + end + + def sanitize(tid) + tid = tid.upcase + return tid if plausible?(tid) + tid.scan(/(\w{2})(\w{2})/).map(&:reverse).join.tap do |reversed| + raise ArgumentError, 'Implausible TID' unless plausible?(reversed) + end + end + + def plausible?(tid) + tid =~ /\AE2[0-9A-F]{22}\z/ + end + + def bit_set?(idx) + bits[idx] == '1' + end + + def bits + @bits ||= @tid.to_i(16).to_s(2) + end +end diff --git a/spec/lib/rfid_tid_spec.rb b/spec/lib/rfid_tid_spec.rb new file mode 100644 index 0000000..bd2df89 --- /dev/null +++ b/spec/lib/rfid_tid_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +require 'rfid_tid' + +RSpec.describe RfidTid do + + it 'ensures big endianness' do + expect(RfidTid.new('80E2051100208A267E868E08').to_s).to eq('E28011052000268A867E088E') + expect(RfidTid.new('E28011052000268A867E088E').to_s).to eq('E28011052000268A867E088E') + end +end diff --git a/spec/requests/api/v1/transactions_spec.rb b/spec/requests/api/v1/transactions_spec.rb index d2b4f5d..6d9ca51 100644 --- a/spec/requests/api/v1/transactions_spec.rb +++ b/spec/requests/api/v1/transactions_spec.rb @@ -90,6 +90,37 @@ def fake_facade_response(success = true, result = {}) api_request(garage_system, :put, api_path, params) expect(last_response.status).to eq(204) end + + context 'CardAccountMapping mismatch on started transaction' do + let(:params) do + { + CarParkId: carpark.carpark_id, + AccountKey: customer_account_mapping.account_key, + Media: { + MediaId: rfid_tag.tag_number, + MediaKey: 'some unknown MediaKey', + MediaType: 255 + }, + DriveIn: { + DateTime: entered_at.iso8601, + DeviceNumber: device_id, + InfoId: nil, + Media: { + MediaId: rfid_tag.uid, + MediaKey: nil, + MediaType: 1 + }, + Status: 1 + }, + Status: 0 + } + end + + it 'returns 204' do + api_request(garage_system, :put, api_path, params) + expect(last_response.status).to eq(204) + end + end end context 'with invalid params' do