acube-rails 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: efa4cde8156a62daa4d25ae2da27e6f71be7b5ed36b27f97ad77fc3a0d961e1a
4
+ data.tar.gz: 262a5d9937aff89409b5d5c20093ed63bc81016db16c1eb4909cf3be5a6e6d4f
5
+ SHA512:
6
+ metadata.gz: 6c204b4482d57e1acf2be4ed401d5f91e122aea8e541de4f9d40a6061604fb61666871c64a51bf483c8a44a3b8cb29244f83c999aa35dc93094e4407ad8ba6cf
7
+ data.tar.gz: 273f3cf9e669898581b5083ed102344248e5f30b421bf33131edf8684328cd5b8c10ae560e994e39303612abd1c6fb69d0bd357316d8ff31327191b6b8c1412a
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2023 Pietro Moro
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # ACube API - Rails [WIP]
2
+ Wrapper library around the ACube API for ruby on rails. Quickly set up resources to manage invoices.
3
+
4
+ ## Usage
5
+ This will copy over the initializer file to your rails application and the necessary migrations:
6
+ ```
7
+ $ bin/rails g a_cube:install
8
+ ```
9
+
10
+ In the initializer file you have to set at the very least the following:
11
+ ```ruby
12
+ config.username = "your-login-email"
13
+ config.password = "your-login-password"
14
+
15
+ config.invoice_endpoint = ENV.fetch('ACUBE_INVOICE_ENDPOINT', "https://api-sandbox.acubeapi.com")
16
+ config.common_endpoint = ENV.fetch('ACUBE_COMMON_ENDPOINT', "https://common-sandbox.api.acubeapi.com")
17
+ ```
18
+
19
+ For a complete list of configuration options, see the initializer file.
20
+ You have to designate two models to be used that will serve as the supplier/consumer contacts.
21
+ These will take care of the mapping between your application and the ACube API.
22
+ ```ruby
23
+ include ACube::Support::Supplier
24
+
25
+ as_supplier do |s|
26
+ s.first_name = "..."
27
+ s.last_name = :last_name
28
+ end
29
+ ```
30
+ String means constant value, symbol means method name on the model that will get called when the invoice is created.
31
+
32
+ ```ruby
33
+ include ACube::Support::Consumer
34
+
35
+ as_custoemr do |c|
36
+ c.first_name = "..."
37
+ c.last_name = :last_name
38
+ end
39
+ ```
40
+ For a full list of supported attributes, see the relevant file.
41
+
42
+ The last model is the one that will be associated with the invoices, so the payment model per say.
43
+ ```ruby
44
+ class Payment < ApplicationRecord
45
+ has_one_invoice :invoice
46
+
47
+ as_transaction do |t|
48
+ t.amount = :amount
49
+ t.currency = "EUR"
50
+ t.payment_date = :created_at
51
+ end
52
+ end
53
+ ```
54
+ The `has_one_invoice` method will create the association between the payment and the invoice.
55
+ The `as_transaction` method will create the mapping between the payment and the invoice.
56
+
57
+ The last step is to actually publish the invoice to the ACube API.
58
+ ```ruby
59
+ # In your controller somewhere
60
+ def create
61
+ @payment = Payment.new(payment_params)
62
+ supplier = Supplier.new(...)
63
+ consumer = Consumer.new(...)
64
+
65
+ if @payment.save
66
+ @payment.publish_invoice(supplier, consumer, :FPR12)
67
+ # ...
68
+ end
69
+ end
70
+ ```
71
+
72
+ ## Installation
73
+ Add this line to your application's Gemfile:
74
+
75
+ ```ruby
76
+ gem "acube-rails", require: "acube"
77
+ ```
78
+
79
+ And then execute:
80
+ ```bash
81
+ $ bundle
82
+ ```
83
+
84
+ Or install it yourself as:
85
+ ```bash
86
+ $ gem install acube-rails
87
+ ```
88
+
89
+ ## Contributing
90
+ Contribution directions go here.
91
+
92
+ ## License
93
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ACube
4
+ class InvoiceRecord < ACube.invoice_base_class.constantize
5
+ self.table_name = 'acube_invoice_records'
6
+
7
+ belongs_to :record, polymorphic: true, touch: true
8
+ has_one_attached :pdf
9
+
10
+ enum format: %i[ FPA12 FPR12 ]
11
+ enum kind: %i[ TD01 TD02 TD03 TD04 TD05 TD06 TD16 TD17 TD18 TD19 TD20 TD21 TD22 TD23 TD24 TD25 TD26 TD27 TD28 ]
12
+
13
+ enum status: { notification_error: -4, download_error: -3, creation_error: -2, error: -1, created: 0, sent: 1, downloaded: 2, not_received: 3, rejected: 4, delivered: 5 }
14
+ end
15
+ end
16
+
17
+ ActiveSupport.run_load_hooks :acube_invoice_record, ACube::InvoiceRecord
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ACube
4
+ class Record < ActiveRecord::Base # :nodoc:
5
+ self.abstract_class = true
6
+ end
7
+ end
8
+
9
+ ActiveSupport.run_load_hooks :acube_record, ACube::Record
@@ -0,0 +1,9 @@
1
+ Rails.autoloaders.each do |autoloader|
2
+ autoloader.inflector.inflect(
3
+ 'acube' => 'ACube'
4
+ )
5
+ end
6
+
7
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
8
+ inflect.acronym "ACube"
9
+ end
@@ -0,0 +1,7 @@
1
+ # config/initializers/zeitwerk.rb
2
+
3
+ Rails.autoloaders.each do |autoloader|
4
+ autoloader.inflector.inflect(
5
+ 'acube' => 'ACube'
6
+ )
7
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ ACube::Engine.routes.draw do
2
+ end
@@ -0,0 +1,46 @@
1
+ class CreateACubeInvoiceRecords < ActiveRecord::Migration[7.0]
2
+ def up
3
+ if connection.adapter_name.downcase != 'postgresql'
4
+ raise "This migration is only compatible with PostgreSQL at the moment."
5
+ end
6
+
7
+ primary_key_type, foreign_key_type = primary_and_foreign_key_types
8
+
9
+ create_table :acube_invoice_records, id: primary_key_type do |t|
10
+ t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
11
+
12
+ t.string :name, null: false
13
+ t.string :webhook_uuid
14
+ t.integer :status, null: false, default: 0
15
+ t.integer :format, null: false
16
+ t.integer :kind, null: false
17
+ t.string :progressive, null: false
18
+ t.text :json_body, size: :long
19
+ t.text :xml_body, size: :long
20
+
21
+ t.timestamps
22
+
23
+ t.index [ :record_type, :record_id, :name ], name: "index_acube_rails_acube_invoice_records_uniqueness", unique: true
24
+ t.index :progressive, unique: true
25
+ t.index :webhook_uuid, unique: true
26
+ end
27
+
28
+ execute <<-SQL
29
+ CREATE SEQUENCE acube_invoice_records_progressive_seq START 1 INCREMENT 1 OWNED BY acube_invoice_records.progressive;
30
+ SQL
31
+ end
32
+
33
+ def down
34
+ drop_table :acube_invoice_records
35
+ execute "DROP SEQUENCE IF EXISTS acube_invoice_records_progressive_seq;"
36
+ end
37
+
38
+ private
39
+ def primary_and_foreign_key_types
40
+ config = Rails.configuration.generators
41
+ setting = config.options[config.orm][:primary_key_type]
42
+ primary_key_type = setting || :primary_key
43
+ foreign_key_type = setting || :bigint
44
+ [primary_key_type, foreign_key_type]
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ module ACube
2
+ module Attribute
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def has_one_invoice(name, strict_loading: strict_loading_by_default)
7
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
8
+ def #{name}
9
+ invoice_record_#{name} || build_invoice_record_#{name}
10
+ end
11
+
12
+ def publish_#{name}!(supplier, customer, format)
13
+ builder = ACube::Invoicer.from(supplier: supplier, customer: customer, invoice: self, format: format)
14
+ builder.create_invoice(self, "#{name}")
15
+ end
16
+
17
+ def #{name}?
18
+ invoice_record_#{name}.present?
19
+ end
20
+ CODE
21
+
22
+ include ACube::Support::Transaction
23
+ has_one :"invoice_record_#{name}", -> { where(name: name) }, class_name: 'ACube::InvoiceRecord', as: :record, inverse_of: :record, autosave: true, dependent: :destroy, strict_loading: strict_loading
24
+ end
25
+
26
+ def with_all_invoice_records
27
+ eager_load(invoice_record_association_names)
28
+ end
29
+
30
+ def invoice_record_association_names
31
+ reflect_on_all_associations(:has_one).collect(&:name).select { |n| n.start_with?("invoice_record_") }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ module ACube
2
+ module Endpoint
3
+ class Auth < CommonBase
4
+ def login
5
+ response = connection.post do |req|
6
+ req.url "/login"
7
+ req.body = {
8
+ email: ACube.username,
9
+ password: ACube.password
10
+ }.to_json
11
+ end
12
+
13
+ if response.success?
14
+ token = JSON.parse(response.body)["token"]
15
+ Rails.cache.write(ACube.auth_token_cache_key, token, expires_in: 20.hours)
16
+ return token
17
+ else
18
+ raise "Login failed: #{response.body} -- #{response.inspect}"
19
+ end
20
+ end
21
+
22
+ def logout
23
+ Rails.cache.delete(ACube.auth_token_cache_key)
24
+ end
25
+
26
+ def token!
27
+ Rails.cache.fetch(ACube.auth_token_cache_key) do
28
+ login
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ require 'faraday'
2
+
3
+ module ACube
4
+ module Endpoint
5
+ class CommonBase
6
+ attr_reader :connection
7
+
8
+ def initialize
9
+ @connection = Faraday.new(
10
+ url: ACube.common_endpoint,
11
+ headers: {'Content-Type' => 'application/json'}
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ module ACube
2
+ module Endpoint
3
+ class Invoices < ItApiBase
4
+ def create(invoice_xml)
5
+ response = connection.post do |req|
6
+ req.url "/invoices"
7
+ req.body = invoice_xml
8
+ end
9
+
10
+ if response.success?
11
+ return JSON.parse(response.body)["uuid"]
12
+ else
13
+ raise "Invoice creation failed: #{response.body} -- #{response.inspect} "
14
+ end
15
+ end
16
+
17
+ def download(uuid)
18
+ response = connection.get do |req|
19
+ req.url "/invoices/#{uuid}"
20
+ req.headers['Content-Type'] = 'application/pdf'
21
+ req.headers['X-PrintTheme'] = ACube.invoice_print_theme || 'standard'
22
+ end
23
+
24
+ if response.success?
25
+ return StringIO.new(response.body)
26
+ else
27
+ raise "Invoice download failed: #{response.body} -- #{response.inspect}"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ require 'faraday'
2
+
3
+ module ACube
4
+ module Endpoint
5
+ class ItApiBase
6
+ attr_reader :connection
7
+
8
+ def initialize
9
+ @connection = Faraday.new(
10
+ url: ACube.invoice_endpoint,
11
+ headers: {
12
+ 'Content-Type' => 'application/xml',
13
+ 'Authorization' => 'Bearer ' + ACube::Endpoint::Auth.new.token!,
14
+ 'X-SendAsync' => 'true'
15
+ }
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ require "rails"
2
+ require "active_record/railtie"
3
+ require "action_controller/railtie"
4
+ require "active_storage/engine"
5
+
6
+ require "acube"
7
+
8
+ module ACube
9
+ class Engine < ::Rails::Engine
10
+ isolate_namespace ACube
11
+ config.eager_load_namespaces << ACube
12
+ engine_name "acube"
13
+
14
+ config.autoload_paths << "#{root}/app/models"
15
+
16
+ initializer "acube.attribute" do
17
+ ActiveSupport.on_load(:active_record) do
18
+ include ACube::Attribute
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,116 @@
1
+ module ACube
2
+ class Invoicer
3
+ attr_accessor :supplier, :customer, :invoice
4
+ attr_accessor :header, :document
5
+
6
+ def self.from(supplier:, customer:, invoice:, format: :FPR12)
7
+ raise "Format #{format} not supported" unless ACube::InvoiceRecord.formats.include?(format)
8
+ new(supplier, customer, invoice, format)
9
+ end
10
+
11
+ def create_invoice(invoice_base_record, name)
12
+ progressive_val = ACube::InvoiceRecord.connection.execute("SELECT nextval('acube_invoice_records_progressive_seq') FROM acube_invoice_records_progressive_seq").first["nextval"]
13
+ progressive_string = ACube.progressive_string.call(progressive_val)
14
+ document.fill_with(transmission_format: @format, progressive: progressive_string)
15
+ xml_body = document.to_xml
16
+
17
+ invoice_record = ACube::InvoiceRecord.create!(
18
+ record: invoice_base_record,
19
+ name: name,
20
+ format: @format,
21
+ kind: invoice.document_kind,
22
+ status: :created,
23
+ progressive: progressive_string,
24
+ xml_body: xml_body,
25
+ )
26
+
27
+ begin
28
+ uuid = ACube::Endpoint::Invoices.new.create(xml_body)
29
+ invoice_record.update_column(:webhook_uuid, uuid)
30
+ rescue => e
31
+ invoice_record.update_column(:status, :creation_error)
32
+ raise e
33
+ end
34
+ end
35
+
36
+ def regenerate_invoice(invoice_record_id, also_send: false)
37
+ invoice_record = ACube::InvoiceRecord.find(invoice_record_id)
38
+ raise "This Invoice was already sent to ACube" if invoice_record.status != "creation_error"
39
+
40
+ document.fill_with(transmission_format: invoice_record.format, progressive: invoice_record.progressive)
41
+ xml_body = document.to_xml
42
+
43
+ invoice_record.update_columns(
44
+ status: :created,
45
+ xml_body: xml_body,
46
+ )
47
+
48
+ if also_send
49
+ begin
50
+ uuid = ACube::Endpoint::Invoices.new.create(xml_body)
51
+ invoice_record.update_column(:webhook_uuid, uuid)
52
+ rescue => e
53
+ invoice_record.update_column(:status, :creation_error)
54
+ raise e
55
+ end
56
+ end
57
+ end
58
+
59
+ def self.retry_invoice_sending(invoice_record_id)
60
+ invoice_record = ACube::InvoiceRecord.find(invoice_record_id)
61
+ raise "This Invoice was already sent to ACube" if invoice_record.status != "creation_error"
62
+
63
+ begin
64
+ uuid = ACube::Endpoint::Invoices.new.create(invoice_record.xml_body)
65
+ invoice_record.update_column(:webhook_uuid, uuid)
66
+ rescue => e
67
+ invoice_record.update_column(:status, :creation_error)
68
+ raise e
69
+ end
70
+ end
71
+
72
+ def self.udate_invoice_attributes(invoice_id, json_body)
73
+ invoice_record = ACube::InvoiceRecord.find_by(webhook_uuid: invoice_id)
74
+ invoice_record.update_column(:json_body, json_body)
75
+
76
+ begin
77
+ downloaded_pdf = ACube::Endpoint::Invoices.new.download(invoice_record.webhook_uuid)
78
+ downloaded_pdf.rewind
79
+ invoice_record.pdf.attach(io: downloaded_pdf, filename: "#{invoice_record.webhook_uuid}-invoice.pdf", content_type: 'application/pdf')
80
+ invoice_record.update_column(:status, :downloaded)
81
+ rescue => e
82
+ invoice_record.update_column(:status, :download_error)
83
+ raise e
84
+ end
85
+ end
86
+
87
+ def self.update_invoice_status(webhook_body)
88
+ notification = JSON.parse(webhook_body, symbolize_names: true)
89
+ invoice_record = ACube::InvoiceRecord.find_by(webhook_uuid: notification[:notification][:invoice_uuid])
90
+
91
+ status = case notification[:notification][:type]
92
+ when "MC" then :not_received
93
+ when "AT" then :not_received
94
+ when "RC" then :delivered
95
+ when "NS" then :rejected
96
+ else :notification_error
97
+ end
98
+
99
+ invoice_record.update_column(:status, status)
100
+ end
101
+
102
+ private
103
+ def initialize(supplier, customer, invoice, format)
104
+ @format = format
105
+
106
+ @supplier = ACube::Schema::Header::Supplier.from(supplier)
107
+ @customer = ACube::Schema::Header::Customer.from(customer)
108
+ @invoice = ACube::Schema::Body.from(invoice)
109
+
110
+ @invoice_base_record = invoice
111
+
112
+ @header = ACube::Schema::Header::Header.new(@supplier, @customer)
113
+ @document = ACube::Schema::Document.new(@header, @invoice)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,96 @@
1
+ module ACube
2
+ module Schema
3
+ class Body
4
+ DOCUMENT_KINDS = %w[TD01 TD02 TD03 TD04 TD05 TD06 TD16 TD17 TD18 TD19 TD20 TD21 TD22 TD23 TD24 TD25 TD26 TD27 TD28].freeze
5
+ PAYMENT_METHODS = %w[MP01 MP02 MP03 MP04 MP05 MP06 MP07 MP08 MP09 MP10 MP11 MP12 MP13 MP14 MP15 MP16 MP17 MP18 MP19 MP20 MP21 MP22 MP23].freeze
6
+
7
+ attr_accessor :document_kind, :date
8
+ attr_accessor :total_price
9
+ attr_accessor :connected_progressive
10
+ attr_accessor :description
11
+ attr_accessor :quantity
12
+ attr_accessor :causal
13
+ attr_accessor :payment_max_date
14
+ attr_accessor :payment_terms, :payment_method
15
+ attr_reader :progressive
16
+
17
+ def self.from(invoice)
18
+ new.tap do |body|
19
+ invoice.transaction_data.each do |key, value|
20
+ value = value.is_a?(Symbol) ? invoice.send(value) : value
21
+ body.send("#{key}=", value)
22
+ end
23
+ end
24
+ end
25
+
26
+ def set_progressive(progressive)
27
+ @progressive = progressive
28
+ end
29
+
30
+ def to_xml
31
+ Nokogiri::XML::Builder.new do |xml|
32
+ xml.FatturaElettronicaBody {
33
+ xml.DatiGenerali {
34
+ xml.DatiGeneraliDocumento {
35
+ xml.TipoDocumento document_kind
36
+ xml.Divisa "EUR"
37
+ xml.Data date.strftime("%Y-%m-%d")
38
+ xml.Numero progressive
39
+ xml.ImportoTotaleDocumento total_price
40
+ xml.Causale causal if causal
41
+ }
42
+
43
+ if (document_kind == :TD04)
44
+ xml.DatiFattureCollegate {
45
+ xml.IdDocumento connected_progressive
46
+ }
47
+ end
48
+ }
49
+
50
+ xml.DatiBeniServizi {
51
+ xml.DettaglioLinee {
52
+ xml.NumeroLinea 1
53
+ xml.Descrizione description
54
+ xml.Quantita ("%f" % quantity.to_f)
55
+ xml.PrezzoUnitario ("%f" % unitary_price.to_f)
56
+ xml.PrezzoTotale ("%f" % price_no_vat.to_f)
57
+ xml.AliquotaIVA ("%.2f" % (ACube.vat_amount * 100).to_f)
58
+ }
59
+
60
+ xml.DatiRiepilogo {
61
+ xml.AliquotaIVA ("%.2f" % (ACube.vat_amount * 100).to_f)
62
+ xml.ImponibileImporto ("%f" % price_no_vat.to_f)
63
+ xml.Imposta ("%f" % vat_amount.to_f)
64
+ xml.EsigibilitaIVA "I"
65
+ }
66
+ }
67
+
68
+ xml.DatiPagamento {
69
+ xml.CondizioniPagamento payment_terms
70
+ xml.DettaglioPagamento {
71
+ xml.ModalitaPagamento payment_method
72
+ xml.DataScadenzaPagamento payment_max_date.strftime("%Y-%m-%d")
73
+ xml.ImportoPagamento ("%f" % total_price.to_f)
74
+ }
75
+ }
76
+ }
77
+ end.to_xml(save_with: 2)
78
+ end
79
+
80
+ private
81
+ def unitary_price
82
+ unitary = total_price / quantity
83
+ unitary_vat = unitary * ACube.vat_amount
84
+ unitary - unitary_vat
85
+ end
86
+
87
+ def price_no_vat
88
+ total_price - vat_amount
89
+ end
90
+
91
+ def vat_amount
92
+ total_price * ACube.vat_amount
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,39 @@
1
+ module ACube
2
+ module Schema
3
+ class Document
4
+ TRANSMISSION_FORMATS = %w[FPR12 FPA12].freeze
5
+
6
+ attr_accessor :body, :header
7
+ attr_accessor :progressive, :transmission_format
8
+
9
+ def initialize(header, body)
10
+ @header = header
11
+ @body = body
12
+ end
13
+
14
+ def fill_with(transmission_format:, progressive:)
15
+ @transmission_format = transmission_format
16
+ @progressive = progressive
17
+
18
+ header.transmission_format = transmission_format
19
+ header.progressive = progressive
20
+ body.set_progressive(progressive)
21
+ end
22
+
23
+ def to_xml
24
+ Nokogiri::XML::Builder.new(encoding: 'UTF-8', namespace_inheritance:false) do |xml|
25
+ xml["p"].FatturaElettronica(
26
+ "versione" => header.transmission_format,
27
+ "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#",
28
+ "xmlns:p" => "http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2",
29
+ "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
30
+ "xsi:schemaLocation" => "http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2 http://www.fatturapa.gov.it/export/fatturazione/sdi/fatturapa/v1.2/Schema_del_file_xml_FatturaPA_versione_1.2.xsd"
31
+ ) do
32
+ xml << header.to_xml
33
+ xml << body.to_xml
34
+ end
35
+ end.to_xml
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,49 @@
1
+ module ACube
2
+ module Schema
3
+ module Header
4
+ class Customer
5
+ attr_accessor :vat_fiscal_id
6
+ attr_accessor :fiscal_code
7
+ attr_accessor :first_name, :last_name, :denomination, :title, :eori_code
8
+ attr_accessor :address, :civic_number, :zip, :city, :province, :nation
9
+
10
+ def self.from(customer)
11
+ new.tap do |cust|
12
+ customer.customer_data.each do |key, value|
13
+ value = value.is_a?(Symbol) ? customer.send(value).to_s : value.to_s
14
+ cust.send("#{key}=", value)
15
+ end
16
+ end
17
+ end
18
+
19
+ def to_xml
20
+ Nokogiri::XML::Builder.new do |xml|
21
+ xml.CessionarioCommittente {
22
+ xml.DatiAnagrafici {
23
+ xml.CodiceFiscale fiscal_code
24
+ xml.Anagrafica {
25
+ if (first_name && last_name)
26
+ xml.Nome first_name
27
+ xml.Cognome last_name
28
+ else
29
+ xml.Denominazione denomination
30
+ end
31
+ xml.Titolo title if title
32
+ xml.CodEORI eori_code if eori_code
33
+ }
34
+ }
35
+ xml.Sede {
36
+ xml.Indirizzo address
37
+ xml.NumeroCivico civic_number if civic_number
38
+ xml.CAP zip
39
+ xml.Comune city
40
+ xml.Provincia province
41
+ xml.Nazione nation
42
+ }
43
+ }
44
+ end.to_xml(save_with: 2)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,36 @@
1
+ module ACube
2
+ module Schema
3
+ module Header
4
+ class Header
5
+ attr_accessor :supplier, :customer
6
+ attr_accessor :transmission_format
7
+ attr_accessor :progressive
8
+
9
+ def initialize(supplier, customer)
10
+ @supplier = supplier
11
+ @customer = customer
12
+ end
13
+
14
+ def to_xml
15
+ Nokogiri::XML::Builder.new do |xml|
16
+ xml.FatturaElettronicaHeader {
17
+ xml.DatiTrasmissione {
18
+ xml.IdTrasmittente {
19
+ xml.IdPaese ACube.transmission_nation_id
20
+ xml.IdCodice ACube.transmission_id_code
21
+ }
22
+
23
+ xml.ProgressivoInvio progressive
24
+ xml.FormatoTrasmissione transmission_format
25
+ xml.CodiceDestinatario "0000000"
26
+ }
27
+
28
+ xml << supplier.to_xml
29
+ xml << customer.to_xml
30
+ }
31
+ end.to_xml(save_with: 2)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,65 @@
1
+ module ACube
2
+ module Schema
3
+ module Header
4
+ class Supplier
5
+ FISCAL_REGIMES = %w[RF01 RF02 RF03 RF04 RF05 RF06 RF07 RF08 RF09 RF10 RF11 RF12 RF13 RF14 RF15 RF16 RF17 RF18 RF19].freeze
6
+
7
+ attr_accessor :id_nation, :id_tax_code
8
+ attr_accessor :fiscal_code
9
+ attr_accessor :first_name, :last_name, :denomination, :title, :eori_code
10
+ attr_accessor :albo_professional, :aldo_province, :albo_subscription, :albo_subscription_date
11
+ attr_accessor :fiscal_regime
12
+ attr_accessor :address, :civic_number, :zip, :city, :province, :nation
13
+
14
+ def self.from(supplier)
15
+ new.tap do |supp|
16
+ supplier.supplier_data.each do |key, value|
17
+ value = value.is_a?(Symbol) ? supplier.send(value).to_s : value.to_s
18
+ supp.send("#{key}=", value)
19
+ end
20
+ end
21
+ end
22
+
23
+ def to_xml
24
+ Nokogiri::XML::Builder.new do |xml|
25
+ xml.CedentePrestatore {
26
+ xml.DatiAnagrafici {
27
+ xml.IdFiscaleIVA {
28
+ xml.IdPaese id_nation
29
+ xml.IdCodice id_tax_code
30
+ }
31
+
32
+ xml.Anagrafica {
33
+ if (first_name && last_name)
34
+ xml.Nome first_name
35
+ xml.Cognome last_name
36
+ else
37
+ xml.Denominazione denomination
38
+ end
39
+ xml.Titolo title if title
40
+ xml.CodEORI eori_code if eori_code
41
+ }
42
+
43
+ xml.RegimeFiscale fiscal_regime
44
+ xml.CodiceFiscale fiscal_code if fiscal_code
45
+
46
+ xml.AlboProfessionale albo_professional if albo_professional
47
+ xml.ProvinciaAlbo aldo_province if aldo_province
48
+ xml.NumeroIscrizioneAlbo albo_subscription if albo_subscription
49
+ xml.DataIscrizioneAlbo albo_subscription_date if albo_subscription_date
50
+ }
51
+ xml.Sede {
52
+ xml.Indirizzo address
53
+ xml.NumeroCivico civic_number if civic_number
54
+ xml.CAP zip
55
+ xml.Comune city
56
+ xml.Provincia province
57
+ xml.Nazione nation
58
+ }
59
+ }
60
+ end.to_xml(save_with: 2)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,45 @@
1
+ module ACube
2
+ class SignatureChecker
3
+ class InvalidSignatureError < StandardError; end
4
+
5
+ HASH_ALGORIGHTM = "sha256"
6
+ DEFAULT_KEY = "acube"
7
+ DEFAULT_GPG = <<-GPG
8
+ -----BEGIN PUBLIC KEY-----
9
+ MCowBQYDK2VwAyEAvZlhiFh4aORWSC9hKZvZyKYgn2g2VeSguWoxu4fbqRI=
10
+ -----END PUBLIC KEY-----
11
+ GPG
12
+
13
+ def self.verify_signature(request, payload)
14
+ new(request.headers, payload).verify_signature
15
+ end
16
+
17
+ attr_reader :headers, :payload
18
+ attr_accessor :signature, :input, :digest
19
+ def initialize(headers, payload)
20
+ @headers = headers
21
+ if (!ACube.webhook_secret_key.nil? && !ACube.webhook_secret.nil?)
22
+ raise InvalidSignatureError unless headers.has_key?("Authorization") && headers["Authorization"] == "#{ACube.webhook_secret_key} #{ACube.webhook_secret}"
23
+ end
24
+
25
+ raise InvalidSignatureError unless headers.has_key?("signature") && headers.has_key?("signature-input") && headers.has_key?("signature-digest")
26
+ signature, input, digest = headers["signature"], headers["signature-input"], headers["signature-digest"]
27
+
28
+ @payload = payload
29
+ end
30
+
31
+ def verify_signature
32
+ raw_data = url + params.sort.join
33
+ OpenSSL::HMAC.digest(HASH_ALGORITHM, signature_gpg, raw_data)
34
+ end
35
+
36
+ private
37
+ def signature_key
38
+ ACube.webhook_signature_key || DEFAULT_KEY
39
+ end
40
+
41
+ def signature_gpg
42
+ ACube.webhook_signature_gpg || DEFAULT_GPG
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,42 @@
1
+ module ACube
2
+ module Support
3
+ module Customer
4
+ extend ActiveSupport::Concern
5
+ cattr_reader :customer_data
6
+
7
+ included do
8
+ protected
9
+ def self.as_customer(&block)
10
+ config = CustomerBuilder.new
11
+ yield(config)
12
+ @@customer_data = config.finalize.dup
13
+ end
14
+ end
15
+
16
+ class CustomerBuilder
17
+ @@attributes = ACube::Schema::Header::Customer.instance_methods.select {|m| m.ends_with?("=") && m.starts_with?(/\w/) }
18
+ @customer_data = {}
19
+
20
+ def initialize
21
+ @customer_data = {}
22
+ end
23
+
24
+ def finalize
25
+ @customer_data
26
+ end
27
+
28
+ def method_missing(method, value)
29
+ if (@@attributes.include?(method))
30
+ @customer_data[method[0..-2]] = value
31
+ else
32
+ super
33
+ end
34
+ end
35
+ end
36
+
37
+ def to_customer
38
+ ACube::Schema::Header::Customer.from(self)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ module ACube
2
+ module Support
3
+ module Supplier
4
+ extend ActiveSupport::Concern
5
+ cattr_reader :supplier_data
6
+
7
+ included do
8
+ protected
9
+ def self.as_supplier(&block)
10
+ config = SupplierBuilder.new
11
+ yield(config)
12
+ @@supplier_data = config.finalize.dup
13
+ end
14
+ end
15
+
16
+ class SupplierBuilder
17
+ @@attributes = ACube::Schema::Header::Supplier.instance_methods.select {|m| m.ends_with?("=") && m.starts_with?(/\w/) }
18
+ @supplier_data = {}
19
+
20
+ def initialize
21
+ @supplier_data = {}
22
+ end
23
+
24
+ def finalize
25
+ @supplier_data
26
+ end
27
+
28
+ def method_missing(method, value)
29
+ if (@@attributes.include?(method))
30
+ @supplier_data[method[0..-2]] = value
31
+ else
32
+ super
33
+ end
34
+ end
35
+ end
36
+
37
+ def to_supplier
38
+ ACube::Schema::Header::Supplier.from(self)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ module ACube
2
+ module Support
3
+ module Transaction
4
+ extend ActiveSupport::Concern
5
+ cattr_reader :transaction_data
6
+
7
+ included do
8
+ protected
9
+ def self.as_transaction(&block)
10
+ config = TransactionBuilder.new
11
+ yield(config)
12
+ @@transaction_data = config.finalize.dup
13
+ end
14
+ end
15
+
16
+ class TransactionBuilder
17
+ @@attributes = ACube::Schema::Body.instance_methods.select {|m| m.ends_with?("=") && m.starts_with?(/\w/) }
18
+ @transaction_data = {}
19
+
20
+ def initialize
21
+ @transaction_data = {}
22
+ end
23
+
24
+ def finalize
25
+ @transaction_data
26
+ end
27
+
28
+ def method_missing(method, value)
29
+ if (@@attributes.include?(method))
30
+ @transaction_data[method[0..-2]] = value
31
+ else
32
+ super
33
+ end
34
+ end
35
+ end
36
+
37
+ def to_transaction
38
+ ACube::Schema::Body.from(self)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module ACube
2
+ VERSION = "0.0.4"
3
+ end
data/lib/acube.rb ADDED
@@ -0,0 +1,84 @@
1
+ require "active_support"
2
+ require "active_support/rails"
3
+
4
+ require "acube/version"
5
+ require "acube/engine"
6
+
7
+ require "zeitwerk"
8
+
9
+ module ACube
10
+ extend ActiveSupport::Autoload
11
+
12
+ mattr_accessor :invoice_endpoint
13
+ mattr_accessor :common_endpoint
14
+
15
+ mattr_accessor :username
16
+ mattr_accessor :password
17
+
18
+ mattr_accessor :invoice_base_class, default: "ApplicationRecord"
19
+
20
+ mattr_accessor :webhook_base_class, default: "ApplicationController"
21
+ mattr_accessor :webhook_endpoint, default: "/acube-api/webhook"
22
+ mattr_accessor :webhook_secret_key
23
+ mattr_accessor :webhook_secret
24
+
25
+ mattr_accessor :webhook_signature_key
26
+ mattr_accessor :webhook_signature_gpg
27
+
28
+ mattr_accessor :auth_token_cache_key, default: "__acube__auth__token"
29
+
30
+ mattr_accessor :invoice_print_theme, default: "standard"
31
+ mattr_accessor :progressive_string, default: -> (number) { "#{number}" }
32
+ mattr_accessor :vat_amount, default: 0.22
33
+
34
+ mattr_accessor :transmission_nation_id, default: "IT"
35
+ mattr_accessor :transmission_id_code, default: "10442360961"
36
+
37
+ def self.configure
38
+ yield(self)
39
+ end
40
+
41
+ autoload :Attribute
42
+ autoload :SignatureChecker
43
+ autoload :Invoicer
44
+
45
+ module Endpoint
46
+ extend ActiveSupport::Autoload
47
+
48
+ autoload :CommonBase
49
+ autoload :ItApiBase
50
+
51
+ autoload :Auth
52
+ autoload :Invoices
53
+ end
54
+
55
+ module Schema
56
+ extend ActiveSupport::Autoload
57
+
58
+ module Header
59
+ extend ActiveSupport::Autoload
60
+
61
+ autoload :Supplier
62
+ autoload :Customer
63
+ autoload :Header
64
+ end
65
+
66
+ autoload :Body
67
+ autoload :Document
68
+ end
69
+
70
+ module Support
71
+ extend ActiveSupport::Autoload
72
+
73
+ autoload :Customer
74
+ autoload :Supplier
75
+ autoload :Transaction
76
+ end
77
+ end
78
+
79
+ loader = Zeitwerk::Loader.for_gem
80
+ loader.ignore("#{__dir__}/generators")
81
+ loader.inflector.inflect(
82
+ "acube" => "ACube"
83
+ )
84
+ loader.setup
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module ACube
6
+ module Generators
7
+ class InstallGenerator < ::Rails::Generators::Base
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ def create_migrations
11
+ rails_command "railties:install:migrations FROM=active_storage,acube", inline: true
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc "Copy over the migration"
4
+ task "acube:install" do
5
+ Rails::Command.invoke :generate, ["acube:install"]
6
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acube-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Pietro Moro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-07-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.7'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 2.7.6
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '2.7'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 2.7.6
47
+ description: ACube api wrapper for rails
48
+ email:
49
+ - pietro@pietromoro.dev
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - MIT-LICENSE
55
+ - README.md
56
+ - Rakefile
57
+ - app/models/acube/invoice_record.rb
58
+ - app/models/acube/record.rb
59
+ - config/application.rb
60
+ - config/initializers/zeitwerk.rb
61
+ - config/routes.rb
62
+ - db/migrate/20230624195146_create_acube_invoice_records.rb
63
+ - lib/acube.rb
64
+ - lib/acube/attribute.rb
65
+ - lib/acube/endpoint/auth.rb
66
+ - lib/acube/endpoint/common_base.rb
67
+ - lib/acube/endpoint/invoices.rb
68
+ - lib/acube/endpoint/it_api_base.rb
69
+ - lib/acube/engine.rb
70
+ - lib/acube/invoicer.rb
71
+ - lib/acube/schema/body.rb
72
+ - lib/acube/schema/document.rb
73
+ - lib/acube/schema/header/customer.rb
74
+ - lib/acube/schema/header/header.rb
75
+ - lib/acube/schema/header/supplier.rb
76
+ - lib/acube/signature_checker.rb
77
+ - lib/acube/support/customer.rb
78
+ - lib/acube/support/supplier.rb
79
+ - lib/acube/support/transaction.rb
80
+ - lib/acube/version.rb
81
+ - lib/generators/a_cube/install/install_generator.rb
82
+ - lib/tasks/acube.rake
83
+ homepage: https://github.com/pietromoro/acube-rails
84
+ licenses:
85
+ - MIT
86
+ metadata:
87
+ homepage_uri: https://github.com/pietromoro/acube-rails
88
+ source_code_uri: https://github.com/pietromoro/acube-rails
89
+ changelog_uri: https://github.com/pietromoro/acube-rails/blob/master/CHANGELOG.md
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubygems_version: 3.3.7
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: ACube api wrapper for rails
109
+ test_files: []