sunat_invoice 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'tributer'
3
+ require_relative 'utils'
4
+
5
+ module SunatInvoice
6
+ class Provider < Tributer
7
+ include Utils
8
+
9
+ attr_accessor :signature_id, :signature_location_id, :uri, :pk_file,
10
+ :cert_file
11
+
12
+ def address(xml)
13
+ xml['cbc'].ID @ubigeo
14
+ xml['cbc'].StreetName @street
15
+ xml['cbc'].CitySubdivisionName @zone
16
+ xml['cbc'].CityName @province
17
+ xml['cbc'].CountrySubentity @department
18
+ xml['cbc'].District @district
19
+ build_country(xml)
20
+ end
21
+
22
+ def build_country(xml)
23
+ xml['cac'].Country do
24
+ xml['cbc'].IdentificationCode country_code
25
+ end
26
+ end
27
+
28
+ def build_name(xml)
29
+ xml['cac'].PartyName do
30
+ xml['cbc'].Name name
31
+ end
32
+ end
33
+
34
+ def build_registration_name(xml)
35
+ xml['cac'].PartyLegalEntity do
36
+ xml['cbc'].RegistrationName name
37
+ end
38
+ end
39
+
40
+ def info(xml)
41
+ xml['cac'].AccountingSupplierParty do
42
+ xml['cbc'].CustomerAssignedAccountID ruc
43
+ xml['cbc'].AdditionalAccountID document_type
44
+ xml['cac'].Party do
45
+ build_name(xml)
46
+ xml['cac'].PostalAddress { address(xml) }
47
+ build_registration_name(xml)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model'
4
+ require_relative 'utils'
5
+ require 'xmldsig'
6
+
7
+ module SunatInvoice
8
+ class Signature < Model
9
+ include Utils
10
+
11
+ C14N_ALGORITHM = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'
12
+ DIGEST_ALGORITHM = 'http://www.w3.org/2000/09/xmldsig#sha1'
13
+ SIGNATURE_ALGORITHM = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
14
+ TRANSFORMATION_ALGORITHM = 'http://www.w3.org/2000/09/xmldsig#enveloped-signature'
15
+
16
+ attr_accessor :provider
17
+
18
+ def signer_data(xml)
19
+ xml['cac'].Signature do
20
+ xml['cbc'].ID provider.signature_id
21
+ xml['cac'].SignatoryParty do
22
+ xml['cac'].PartyIdentification do
23
+ xml['cbc'].ID provider.ruc
24
+ end
25
+ xml['cac'].PartyName do
26
+ xml['cbc'].Name provider.name
27
+ end
28
+ end
29
+ xml['cac'].DigitalSignatureAttachment do
30
+ xml['cac'].ExternalReference do
31
+ xml['cbc'].URI "##{provider.signature_location_id}"
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def sign(invoice_xml)
38
+ Xmldsig::SignedDocument.new(invoice_xml, id_attr: provider.signature_location_id).sign(private_key)
39
+ end
40
+
41
+ def signature_ext(xml)
42
+ ubl_ext(xml) do
43
+ xml['ds'].Signature(Id: provider.signature_location_id) do
44
+ signed_info xml
45
+ signature_value xml
46
+ end
47
+ end
48
+ end
49
+
50
+ def signed_info(xml)
51
+ xml['ds'].SignedInfo do
52
+ xml['ds'].CanonicalizationMethod Algorithm: C14N_ALGORITHM
53
+ xml['ds'].SignatureMethod Algorithm: SIGNATURE_ALGORITHM
54
+ xml['ds'].Reference URI: '' do
55
+ xml['ds'].Transforms do
56
+ xml['ds'].Transform Algorithm: TRANSFORMATION_ALGORITHM
57
+ end
58
+ xml['ds'].DigestMethod Algorithm: DIGEST_ALGORITHM
59
+ xml['ds'].DigestValue
60
+ end
61
+ end
62
+ end
63
+
64
+ def signature_value(xml)
65
+ xml['ds'].SignatureValue
66
+ xml['ds'].KeyInfo do
67
+ xml['ds'].X509Data do
68
+ xml['ds'].X509Certificate encoded_certificate
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def encoded_certificate
76
+ Base64.encode64(certificate.to_der).gsub(/\n/, '')
77
+ end
78
+
79
+ def private_key
80
+ OpenSSL::PKey::RSA.new(File.read(provider.pk_file))
81
+ end
82
+
83
+ def certificate
84
+ OpenSSL::X509::Certificate.new(File.read(provider.cert_file))
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'catalogs'
4
+ require_relative 'utils'
5
+
6
+ module SunatInvoice
7
+ class Tax < Model
8
+ include Utils
9
+
10
+ TAXES = {
11
+ igv: { id: '1000', name: 'IGV', tax_type_code: 'VAT' },
12
+ isc: { id: '2000', name: 'ISC', tax_type_code: 'EXC' },
13
+ other: { id: '9999', name: 'OTROS', tax_type_code: 'OTH' }
14
+ }.freeze
15
+
16
+ attr_accessor :amount, :tax_type, :tax_exemption_reason, :tier_range
17
+
18
+ def initialize(*args)
19
+ super(*args)
20
+ defaults_for_type(tax_type)
21
+ end
22
+
23
+ def defaults_for_type(type)
24
+ case type
25
+ when :igv
26
+ @tax_exemption_reason ||= Catalogs::CATALOG_07.first
27
+ when :isc
28
+ @tier_range ||= Catalogs::CATALOG_08.first
29
+ end
30
+ end
31
+
32
+ def xml(xml, currency)
33
+ xml['cac'].TaxTotal do
34
+ amount_xml(xml['cbc'], 'TaxAmount', amount, currency)
35
+ xml['cac'].TaxSubtotal do
36
+ amount_xml(xml['cbc'], 'TaxAmount', amount, currency)
37
+ tax_category(xml)
38
+ end
39
+ end
40
+ end
41
+
42
+ def tax_category(xml)
43
+ xml['cac'].TaxCategory do
44
+ xml['cbc'].TaxExemptionReasonCode(tax_exemption_reason) if tax_exemption_reason
45
+ xml['cbc'].TierRange(tier_range) if tier_range
46
+ tax_scheme(xml)
47
+ end
48
+ end
49
+
50
+ def tax_scheme(xml)
51
+ xml['cac'].TaxScheme do
52
+ xml['cbc'].ID tax_data(:id)
53
+ xml['cbc'].Name tax_data(:name)
54
+ xml['cbc'].TaxTypeCode tax_data(:tax_type_code)
55
+ end
56
+ end
57
+
58
+ def tax_data(attribute)
59
+ TAXES[tax_type][attribute]
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model'
4
+
5
+ module SunatInvoice
6
+ class Tributer < Model
7
+ attr_accessor :ruc, :name, :document_type, :ubigeo, :street, :zone,
8
+ :province, :department, :district, :country_code
9
+ end
10
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: false
2
+
3
+ module SunatInvoice
4
+ module Utils
5
+ @namespace_path = 'urn:oasis:names:specification:ubl:schema:xsd'
6
+ @sunat_namespace_path = 'urn:sunat:names:specification:ubl:peru:schema:xsd'
7
+
8
+ UBL_NAMESPACES = {
9
+ 'xmlns' => "#{@namespace_path}:Invoice-2",
10
+ 'xmlns:cac' => "#{@namespace_path}:CommonAggregateComponents-2",
11
+ 'xmlns:cbc' => "#{@namespace_path}:CommonBasicComponents-2",
12
+ 'xmlns:ccts' => 'urn:un:unece:uncefact:documentation:2',
13
+ 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#',
14
+ 'xmlns:ext' => "#{@namespace_path}:CommonExtensionComponents-2",
15
+ 'xmlns:qdt' => "#{@namespace_path}:QualifiedDatatypes-2",
16
+ 'xmlns:sac' => "#{@sunat_namespace_path}:SunatAggregateComponents-1",
17
+ 'xmlns:udt' => 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2',
18
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
19
+ }.freeze
20
+
21
+ def ubl_ext(xml, &block)
22
+ xml['ext'].UBLExtension do
23
+ xml['ext'].ExtensionContent(&block)
24
+ end
25
+ end
26
+
27
+ def amount_xml(xml, tag, price, currency)
28
+ xml.send(tag, price, currencyID: currency)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sunat_invoice/invoice'
4
+ require 'sunat_invoice/provider'
5
+ require 'sunat_invoice/customer'
6
+ require 'sunat_invoice/item'
7
+ require 'sunat_invoice/tax'
8
+ require 'sunat_invoice/configuration'
9
+ require 'sunat_invoice/client'
10
+ require 'sunat_invoice/clients/invoice_client'
11
+
12
+ module SunatInvoice
13
+ class << self
14
+ attr_accessor :configuration
15
+ end
16
+
17
+ def self.configure
18
+ self.configuration ||= Configuration.new
19
+ yield(configuration)
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'sunat_invoice'
5
+ s.version = '0.0.1'
6
+ s.summary = 'SOAP client to use SUNAT Electronic Invoice API'
7
+ s.description = s.summary
8
+ s.authors = ['César Carruitero']
9
+ s.email = ['cesar@mozilla.pe']
10
+ s.homepage = 'https://github.com/ccarruitero/sunat_invoice'
11
+ s.license = 'MPL-2.0'
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+
15
+ s.add_dependency 'savon', '~> 2.11.2'
16
+ s.add_dependency 'nokogiri', '~> 1.8'
17
+ s.add_dependency 'rubyzip', '~> 1.2.1'
18
+ s.add_dependency 'xmldsig', '~> 0.6.5'
19
+
20
+ s.add_development_dependency 'cutest', '~> 1.2'
21
+ s.add_development_dependency 'factory_bot', '~> 4.8'
22
+ s.add_development_dependency 'ffaker', '~> 2.7'
23
+ s.add_development_dependency 'pry', '~> 0.11'
24
+ s.add_development_dependency 'rubocop', '~> 0.51'
25
+ end
data/test/factories.rb ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: false
2
+
3
+ FactoryBot.define do
4
+ factory :tax, class: 'SunatInvoice::Tax' do
5
+ amount 20
6
+ tax_type :igv
7
+ end
8
+
9
+ factory :item, class: 'SunatInvoice::Item' do
10
+ quantity 10
11
+ unit_code 'NIU'
12
+ price 20
13
+ price_code '01'
14
+ description 'Grabadora Externo'
15
+ end
16
+
17
+ factory :invoice, class: 'SunatInvoice::Invoice' do
18
+ provider { build(:provider) }
19
+ customer { build(:customer) }
20
+
21
+ initialize_with { new(attributes) }
22
+ end
23
+
24
+ factory :provider, class: 'SunatInvoice::Provider' do
25
+ pk_file File.join(File.dirname(__FILE__), 'certs/pk_file')
26
+ cert_file File.join(File.dirname(__FILE__), 'certs/cert_file')
27
+ ruc FFaker::IdentificationMX.curp
28
+ name FFaker::Company.name
29
+ document_type 6
30
+ ubigeo '14'
31
+ street ''
32
+ zone ''
33
+ province ''
34
+ department ''
35
+ district ''
36
+ country_code ''
37
+
38
+ initialize_with { new(attributes) }
39
+ end
40
+
41
+ factory :customer, class: 'SunatInvoice::Customer' do
42
+ ruc FFaker::IdentificationMX.curp
43
+ name FFaker::Company.name
44
+ document_type 6
45
+
46
+ initialize_with { new(attributes) }
47
+ end
48
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ require 'cutest'
3
+ require 'pry'
4
+ require 'ffaker'
5
+ require 'factory_bot'
6
+ require_relative '../lib/sunat_invoice'
7
+ require_relative 'factories'
8
+
9
+ Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f }
10
+
11
+ SignatureHelper.generate_keys
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ scope 'SunatInvoice::InvoiceClient' do
6
+ setup do
7
+ provider = FactoryBot.build(:provider,
8
+ signature_id: 'signatureST',
9
+ signature_location_id: 'signQWI3',
10
+ ruc: '20100454523',
11
+ name: 'SOPORTE TECNOLOGICO EIRL')
12
+
13
+ customer = FactoryBot.build(:customer,
14
+ ruc: '20293028401',
15
+ name: 'SOME BUSINESS')
16
+ SunatInvoice.configure do |c|
17
+ c.account_ruc = provider.ruc
18
+ c.account_user = 'MODDATOS'
19
+ c.account_password = 'moddatos'
20
+ c.provider = provider
21
+ end
22
+ @client = SunatInvoice::InvoiceClient.new
23
+
24
+ tax = SunatInvoice::Tax.new(amount: 3.6, tax_type: :igv)
25
+ item = FactoryBot.build(:item, taxes: [tax])
26
+ @invoice = SunatInvoice::Invoice.new(provider: provider,
27
+ customer: customer,
28
+ document_number: 'FY02-234')
29
+ @invoice.items << item
30
+ end
31
+
32
+ test 'should use dev service when not define environment' do
33
+ assert_equal @client.wsdl, @client.dev_server
34
+ end
35
+
36
+
37
+ test '#send_invoice success' do
38
+ response = @client.send_invoice(@invoice.xml, @invoice.document_name)
39
+ assert_equal response.http.code, 200
40
+ end
41
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: false
2
+ require_relative 'helper'
3
+ include SunatInvoice
4
+
5
+ setup do
6
+ @provider = FactoryBot.build(:provider)
7
+ @invoice = FactoryBot.build(:invoice, provider: @provider)
8
+ tax = SunatInvoice::Tax.new(amount: 3.6, tax_type: :igv)
9
+ item_attr = { quantity: 10, price: 20, price_code: '01', taxes: [tax] }
10
+ @invoice.items << SunatInvoice::Item.new(item_attr)
11
+ @parsed_xml = Nokogiri::XML(@invoice.xml, &:noblanks)
12
+ end
13
+
14
+ test 'is not broken' do
15
+ assert !@invoice.nil?
16
+ end
17
+
18
+ test 'has namespaces' do
19
+ cbc = 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'
20
+ namespaces = @parsed_xml.css('Invoice').first.namespaces
21
+ assert_equal namespaces['xmlns:cbc'], cbc
22
+ end
23
+
24
+ test 'xml start with invoice tag' do
25
+ assert_equal @parsed_xml.root.name, 'Invoice'
26
+ end
27
+
28
+ test 'has a date' do
29
+ date = @parsed_xml.xpath('//cbc:IssueDate')
30
+ assert_equal date.first.content, DateTime.now.strftime('%Y-%m-%d')
31
+ end
32
+
33
+ test 'has a signature' do
34
+ # /ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/ds:Signature
35
+ signature = @parsed_xml.xpath('//ext:UBLExtensions/ext:UBLExtension')
36
+ assert_equal signature.xpath('//ext:ExtensionContent/ds:Signature').count, 1
37
+
38
+ # /cac:Signature
39
+ signature = @parsed_xml.xpath('//cac:Signature')
40
+ assert_equal signature.count, 1
41
+ end
42
+
43
+ test 'has a registration name' do
44
+ provider = @parsed_xml.xpath('//cac:AccountingSupplierParty/cac:Party')
45
+ name = provider.xpath('//cac:PartyLegalEntity/cbc:RegistrationName')
46
+ assert_equal name.first.content, @provider.name
47
+ end
48
+
49
+ test 'has a name' do
50
+ provider = @parsed_xml.xpath('//cac:AccountingSupplierParty/cac:Party')
51
+ name = provider.xpath('//cac:PartyName/cbc:Name')
52
+ assert_equal name.first.content, @provider.name
53
+ end
54
+
55
+ test 'has an address' do
56
+ provider = @parsed_xml.xpath('//cac:AccountingSupplierParty/cac:Party')
57
+ ubigeo = provider.xpath('//cac:PostalAddress/cbc:ID')
58
+ assert_equal ubigeo.first.content, @provider.ubigeo
59
+
60
+ street = provider.xpath('//cac:PostalAddress/cbc:StreetName')
61
+ assert_equal street.first.content, @provider.street
62
+
63
+ urbanizacion = provider.xpath('//cac:PostalAddress/cbc:CitySubdivisionName')
64
+ assert_equal urbanizacion.first.content, @provider.zone
65
+
66
+ provincia = provider.xpath('//cac:PostalAddress/cbc:CityName')
67
+ assert_equal provincia.first.content, @provider.province
68
+
69
+ # Departamento
70
+ departamento = provider.xpath('//cac:PostalAddress/cbc:CountrySubentity')
71
+ assert_equal departamento.first.content, @provider.department
72
+
73
+ # Distrito
74
+ distrito = provider.xpath('//cac:PostalAddress/cbc:District')
75
+ assert_equal distrito.first.content, @provider.district
76
+
77
+ country_code_path = '//cac:PostalAddress/cac:Country/cbc:IdentificationCode'
78
+ country_code = provider.xpath(country_code_path)
79
+ assert_equal country_code.first.content, @provider.country_code.to_s
80
+ end
81
+
82
+ test 'has a ruc' do
83
+ provider = @parsed_xml.xpath('//cac:AccountingSupplierParty')
84
+ ruc = provider.xpath('//cbc:CustomerAssignedAccountID')
85
+ assert_equal ruc.first.content, @provider.ruc
86
+
87
+ document_type = provider.xpath('//cbc:AdditionalAccountID')
88
+ assert_equal document_type.first.content, @provider.document_type.to_s
89
+ end
90
+
91
+ test 'has an invoice type' do
92
+ invoice_type = @parsed_xml.xpath('//cbc:InvoiceTypeCode')
93
+ assert_equal invoice_type.first.content, @invoice.document_type
94
+ end
95
+
96
+ test 'has a correlative' do
97
+ tags = @parsed_xml.xpath('//cbc:ID')
98
+ correlative = tags.select { |tax| tax.parent.name == 'Invoice' }.first
99
+ assert_equal correlative.content, @invoice.document_number
100
+ end
101
+
102
+ test 'has a customer' do
103
+ customer = @parsed_xml.xpath('//cac:AccountingCustomerParty')
104
+ assert_equal customer.count, 1
105
+ end
106
+
107
+ test 'has at least one item' do
108
+ item = @parsed_xml.xpath('//cac:InvoiceLine')
109
+ assert item.count.positive?
110
+ end
111
+
112
+ test 'has total by kind of sale' do
113
+ tag = '//ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sac:AdditionalInformation'
114
+ additional_info = @parsed_xml.xpath(tag)
115
+ assert additional_info.count.positive?
116
+ assert_equal additional_info.first.children.count, 1
117
+ amount_tag = '//sac:AdditionalMonetaryTotal/cbc:PayableAmount'
118
+ assert_equal additional_info.xpath(amount_tag).first.content.to_f, 200.to_f
119
+ end
120
+
121
+ test 'has total tag' do
122
+ total = @parsed_xml.xpath('//cac:LegalMonetaryTotal')
123
+ assert total.count.positive?
124
+ end
125
+
126
+ test '#calculate_tax_totals' do
127
+ tax = FactoryBot.build(:tax, amount: 15)
128
+ invoice = FactoryBot.build(:invoice)
129
+ 2.times do
130
+ invoice.items << FactoryBot.build(:item, taxes: [tax])
131
+ end
132
+ invoice.calculate_tax_totals
133
+ tax_totals = invoice.instance_variable_get('@tax_totals')
134
+ assert_equal tax_totals.count, 1
135
+ assert_equal tax_totals[:igv], 300
136
+ end
137
+
138
+ test '#get_total_igv_code' do
139
+ invoice = SunatInvoice::Invoice.new
140
+ assert_equal invoice.get_total_igv_code('11'), '1004'
141
+ assert_equal invoice.get_total_igv_code('15'), '1004'
142
+ assert_equal invoice.get_total_igv_code('20'), '1003'
143
+ assert_equal invoice.get_total_igv_code('32'), '1002'
144
+ assert invoice.get_total_igv_code('42').nil?
145
+ end
data/test/item_test.rb ADDED
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'helper'
3
+ include SunatInvoice
4
+
5
+ setup do
6
+ @item = SunatInvoice::Item.new(quantity: 200, price: 69, price_code: '01',
7
+ description: 'item description',
8
+ unit_code: 'NIU')
9
+ @item.taxes << SunatInvoice::Tax.new(amount: 12.42, tax_type: :igv)
10
+ invoice_setup
11
+ end
12
+
13
+ def invoice_setup
14
+ # add item to invoice in order to setup namespaces
15
+ invoice = FactoryBot.build(:invoice)
16
+ invoice.items << @item
17
+ invoice_xml = Nokogiri::XML(invoice.xml, &:noblanks)
18
+ @item_xml = invoice_xml.xpath('//cac:InvoiceLine')
19
+ end
20
+
21
+ test 'not broken' do
22
+ assert !@item.nil?
23
+ end
24
+
25
+ # xml
26
+ test 'has unit code and quantity' do
27
+ quantity = @item_xml.xpath('//cbc:InvoicedQuantity')
28
+ assert quantity.count.positive?
29
+ assert_equal @item.quantity.to_s, quantity.first.content
30
+
31
+ # /Invoice/cac:InvoiceLine/cbc:InvoicedQuantity/@unitCode
32
+ attributes = quantity.first.attributes
33
+ assert_equal attributes.keys, ['unitCode']
34
+ assert_equal attributes['unitCode'].value, @item.unit_code
35
+ end
36
+
37
+ test 'has a description' do
38
+ description = @item_xml.xpath('//cbc:Description')
39
+ assert_equal @item.description, description.first.content
40
+ end
41
+
42
+ test 'has unit prices' do
43
+ price = @item_xml.xpath('//cac:Price/cbc:PriceAmount')
44
+ assert_equal @item.price.to_s, price.first.content
45
+ # /Invoice/cac:InvoiceLine/cac:Price/cbc:PriceAmount/@currencyID
46
+ assert_equal price.first.attributes.keys, ['currencyID']
47
+ assert_equal price.first.attributes['currencyID'].value, 'PEN'
48
+
49
+ ref_pricing = @item_xml.xpath('//cac:PricingReference')
50
+ alt_condition_price = ref_pricing.xpath('//cac:AlternativeConditionPrice')
51
+ assert_equal 1, alt_condition_price.count
52
+
53
+ tag = '//cac:AlternativeConditionPrice/cbc:PriceAmount'
54
+ alt_price = ref_pricing.xpath(tag)
55
+ assert_equal @item.sale_price.to_s, alt_price.first.content
56
+ # //cac:AlternativeConditionPrice/cbc:PriceAmount/@currencyID
57
+
58
+ price_type = alt_condition_price.xpath('//cbc:PriceTypeCode')
59
+ assert_equal @item.price_code, price_type.first.content
60
+ end
61
+
62
+ # taxes
63
+ test 'has taxes' do
64
+ taxes = @item_xml.xpath('//cac:InvoiceLine/cac:TaxTotal')
65
+ assert_equal 1, taxes.count
66
+ end
67
+
68
+ test 'has amount in correct tag' do
69
+ tag_path = '//cac:InvoiceLine/cac:TaxTotal/cbc:TaxAmount'
70
+ amount = @item_xml.xpath(tag_path)
71
+ assert_equal amount.count, 1
72
+ assert_equal amount.first.content, '12.42'
73
+ # cac:TaxTotal/cbc:TaxAmount/@currencyID
74
+ end
75
+
76
+ test 'has correct values in TaxScheme' do
77
+ tag_path = '//cac:TaxTotal/cac:TaxSubtotal/cac:TaxCategory/cac:TaxScheme'
78
+ id = @item_xml.xpath("#{tag_path}/cbc:ID")
79
+ assert_equal id.first.content, '1000'
80
+
81
+ name = @item_xml.xpath("#{tag_path}/cbc:Name")
82
+ assert_equal name.first.content, 'IGV'
83
+
84
+ tax_code = @item_xml.xpath("#{tag_path}/cbc:TaxTypeCode")
85
+ assert_equal tax_code.first.content, 'VAT'
86
+ end
87
+
88
+ scope '#sale_taxes' do
89
+ setup do
90
+ tax = SunatInvoice::Tax.new(amount: 10.8, tax_type: :igv)
91
+ @item = SunatInvoice::Item.new(quantity: 2, price: 60, taxes: [tax])
92
+ end
93
+
94
+ test 'return a hash' do
95
+ assert_equal @item.sale_taxes.class, Hash
96
+ end
97
+
98
+ test 'has tax_type and total tax for item' do
99
+ assert_equal @item.sale_taxes.count, 1
100
+ assert_equal @item.sale_taxes.keys, [:igv]
101
+ assert_equal @item.sale_taxes.values, [21.6]
102
+ end
103
+
104
+ test 'has sum by tax_type' do
105
+ @item.taxes << SunatInvoice::Tax.new(amount: 5, tax_type: :isc)
106
+ assert_equal @item.sale_taxes.count, 2
107
+ assert_equal @item.sale_taxes.keys, [:igv, :isc]
108
+ assert_equal @item.sale_taxes.values, [21.6, 10]
109
+ end
110
+ end
@@ -0,0 +1,30 @@
1
+ module SignatureHelper
2
+ def self.generate_keys
3
+ pk = OpenSSL::PKey::RSA.new 2048
4
+ name = OpenSSL::X509::Name.parse('CN=example.com/C=EE')
5
+ cert = OpenSSL::X509::Certificate.new
6
+ cert.version = 2
7
+ cert.serial = 0
8
+ cert.not_before = Time.now
9
+ cert.not_after = cert.not_before + 1 * 365 * 24 * 60 * 60
10
+ cert.public_key = pk.public_key
11
+ cert.subject = name
12
+ cert.issuer = name
13
+ cert.sign pk, OpenSSL::Digest::SHA1.new
14
+
15
+ unless exists?('pk_file')
16
+ cert_dir = "#{File.dirname(__FILE__)}/../certs"
17
+ Dir.mkdir(cert_dir) unless Dir.exist?(cert_dir)
18
+ File.open(file('pk_file'), 'w') { |f| f.puts pk.to_pem }
19
+ File.open(file('cert_file'), 'w') { |f| f.puts cert.to_pem }
20
+ end
21
+ end
22
+
23
+ def self.file(name)
24
+ File.join(File.dirname(__FILE__), "../certs/#{name}")
25
+ end
26
+
27
+ def self.exists?(name)
28
+ File.exists?(file(name))
29
+ end
30
+ end