acube-rails 0.0.4

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 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: []