From 6fc647273e03a01d20fe15b476e07e2a9f0bcc28 Mon Sep 17 00:00:00 2001 From: Thomas Burkhalter Date: Tue, 13 Feb 2024 16:54:07 +0100 Subject: [PATCH] Revert Feature "SmallInvoice APIv2" This reverts the following commits: - 6b619e032ec8824038f1006bb7de2a248193f156 - 6f0ef9e350e0c5453f9f8f3f558740a6c1635a55 - a6c36bb703f7cb2bfecb8135fdc2017dcb78f849 - d93c5f46c14ad94f2658294917b1fe8a9e625382 - 486e3c21482e14a0672578365109ab8d81e114bb - cce2878169802449dd82dddc64b050fb9e228410 - be11e293c41a9f51a0589127947af8e43aef50d2 - 6dabde663a701fb1f34cba07aef41c1f96829968 - 9edc5db54d6ebe44662176149946052a701d8334 --- Gemfile | 2 + Gemfile.lock | 12 ++ app/domain/invoicing.rb | 8 +- .../invoicing/small_invoice/address_sync.rb | 103 ------------ app/domain/invoicing/small_invoice/api.rb | 154 +++++------------- .../invoicing/small_invoice/client_sync.rb | 82 ++++++++-- .../invoicing/small_invoice/contact_sync.rb | 111 ------------- app/domain/invoicing/small_invoice/entity.rb | 7 - .../invoicing/small_invoice/entity/address.rb | 10 -- .../invoicing/small_invoice/entity/base.rb | 4 - .../invoicing/small_invoice/entity/client.rb | 34 ++++ .../invoicing/small_invoice/entity/contact.rb | 25 +-- .../invoicing/small_invoice/entity/invoice.rb | 63 +++---- .../invoicing/small_invoice/entity/person.rb | 32 ---- .../small_invoice/entity/position.rb | 11 +- .../invoicing/small_invoice/interface.rb | 4 +- .../invoicing/small_invoice/invoice_store.rb | 9 +- .../invoicing/small_invoice/invoice_sync.rb | 53 +++--- app/models/invoice.rb | 16 +- config/locales/models.de-CH.yml | 1 - config/locales/models.de-DE.yml | 1 - config/settings.yml | 6 +- .../small_invoice/address_sync_test.rb | 77 --------- .../invoicing/small_invoice/api_test.rb | 75 --------- .../small_invoice/client_sync_test.rb | 53 ------ .../small_invoice/contact_sync_test.rb | 75 --------- .../invoicing/small_invoice/interface_test.rb | 86 ---------- .../small_invoice/invoice_store_test.rb | 108 ------------ .../small_invoice/invoice_sync_test.rb | 108 ------------ .../files/small_invoice/addresses.json | 10 -- .../fixtures/files/small_invoice/contact.json | 33 ---- .../files/small_invoice/contacts.json | 42 ----- .../fixtures/files/small_invoice/invoice.json | 43 ----- .../files/small_invoice/invoices.json | 52 ------ test/fixtures/files/small_invoice/people.json | 25 --- test/fixtures/files/small_invoice/person.json | 16 -- .../files/small_invoice/positions.json | 10 -- test/support/small_invoice_test_helper.rb | 138 ---------------- test/test_helper.rb | 13 -- 39 files changed, 227 insertions(+), 1485 deletions(-) delete mode 100644 app/domain/invoicing/small_invoice/address_sync.rb delete mode 100644 app/domain/invoicing/small_invoice/contact_sync.rb delete mode 100644 app/domain/invoicing/small_invoice/entity.rb create mode 100644 app/domain/invoicing/small_invoice/entity/client.rb delete mode 100644 app/domain/invoicing/small_invoice/entity/person.rb delete mode 100644 test/domain/invoicing/small_invoice/address_sync_test.rb delete mode 100644 test/domain/invoicing/small_invoice/api_test.rb delete mode 100644 test/domain/invoicing/small_invoice/client_sync_test.rb delete mode 100644 test/domain/invoicing/small_invoice/contact_sync_test.rb delete mode 100644 test/domain/invoicing/small_invoice/interface_test.rb delete mode 100644 test/domain/invoicing/small_invoice/invoice_store_test.rb delete mode 100644 test/domain/invoicing/small_invoice/invoice_sync_test.rb delete mode 100644 test/fixtures/files/small_invoice/addresses.json delete mode 100644 test/fixtures/files/small_invoice/contact.json delete mode 100644 test/fixtures/files/small_invoice/contacts.json delete mode 100644 test/fixtures/files/small_invoice/invoice.json delete mode 100644 test/fixtures/files/small_invoice/invoices.json delete mode 100644 test/fixtures/files/small_invoice/people.json delete mode 100644 test/fixtures/files/small_invoice/person.json delete mode 100644 test/fixtures/files/small_invoice/positions.json delete mode 100644 test/support/small_invoice_test_helper.rb diff --git a/Gemfile b/Gemfile index 8c13b4853..9baa89eb1 100644 --- a/Gemfile +++ b/Gemfile @@ -115,4 +115,6 @@ group :test do gem 'mocha', require: false gem 'rails-controller-testing' gem 'webmock' + gem 'selenium-webdriver' + gem 'webdrivers' end diff --git a/Gemfile.lock b/Gemfile.lock index 2be6a7b40..db988d9c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -582,6 +582,7 @@ GEM ruby-vips (2.2.0) ffi (~> 1.12) ruby2_keywords (0.0.5) + rubyzip (2.3.2) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) @@ -598,6 +599,10 @@ GEM activerecord (>= 3.1) activesupport (>= 3.1) selectize-rails (0.12.6) + selenium-webdriver (4.8.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) sentry-raven (3.1.2) faraday (>= 1.0) simpleidn (0.2.1) @@ -650,11 +655,16 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + webdrivers (5.2.0) + nokogiri (~> 1.6) + rubyzip (>= 1.3.0) + selenium-webdriver (~> 4.0) webmock (3.19.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) webrick (1.8.1) + websocket (1.2.9) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -751,6 +761,7 @@ DEPENDENCIES sdoc seed-fu selectize-rails + selenium-webdriver sentry-raven spring swagger-blocks @@ -759,6 +770,7 @@ DEPENDENCIES validates_by_schema validates_timeliness web-console + webdrivers webmock BUNDLED WITH diff --git a/app/domain/invoicing.rb b/app/domain/invoicing.rb index 31524fee7..b483b8874 100644 --- a/app/domain/invoicing.rb +++ b/app/domain/invoicing.rb @@ -10,9 +10,9 @@ module Invoicing cattr_accessor :instance def self.init - return unless Settings.small_invoice.client_id && Settings.small_invoice.client_secret && !Rails.env.test? - - Invoicing.instance = Invoicing::SmallInvoice::Interface.new - InvoicingSyncJob.schedule if Delayed::Job.table_exists? + if Settings.small_invoice.api_token && !Rails.env.test? + Invoicing.instance = Invoicing::SmallInvoice::Interface.new + InvoicingSyncJob.schedule if Delayed::Job.table_exists? + end end end diff --git a/app/domain/invoicing/small_invoice/address_sync.rb b/app/domain/invoicing/small_invoice/address_sync.rb deleted file mode 100644 index 484d10ffc..000000000 --- a/app/domain/invoicing/small_invoice/address_sync.rb +++ /dev/null @@ -1,103 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module Invoicing - module SmallInvoice - # One-way sync from PuzzleTime to Small Invoice for clients, contacts and billing addresses. - class AddressSync - class << self - def notify_sync_error(error, address = nil) - parameters = address.present? ? record_to_params(address) : {} - parameters[:code] = error.code if error.respond_to?(:code) - parameters[:data] = error.data if error.respond_to?(:data) - Airbrake.notify(error, parameters) if airbrake? - Raven.capture_exception(error, extra: parameters) if sentry? - end - - private - - def airbrake? - ENV['RAILS_AIRBRAKE_HOST'].present? - end - - def sentry? - ENV['SENTRY_DSN'].present? - end - - def record_to_params(record, prefix = 'billing_address') - { - "#{prefix}_id" => record.id, - "#{prefix}_invoicing_key" => record.invoicing_key, - "#{prefix}_shortname" => record.try(:shortname), - "#{prefix}_label" => record.try(:label) || record.to_s, - "#{prefix}_errors" => record.errors.messages, - "#{prefix}_changes" => record.changes - } - end - end - - delegate :notify_sync_error, to: 'self.class' - attr_reader :client, :remote_keys - - class_attribute :rate_limiter - self.rate_limiter = RateLimiter.new(Settings.small_invoice.request_rate) - - def initialize(client, remote_keys = nil) - @client = client - @remote_keys = remote_keys || fetch_remote_keys - end - - def sync - failed = [] - ::BillingAddress.includes(:client).where(client_id: client.id).find_each do |billing_address| - key(billing_address) ? update_remote(billing_address) : create_remote(billing_address) - rescue StandardError => e - failed << billing_address.id - Rails.logger.error e.message - Rails.logger.error e.backtrace - notify_sync_error(e, billing_address) - end - Rails.logger.error "Failed Address Syncs: #{failed.inspect}" if failed.any? - end - - private - - def fetch_remote_keys - api.list(Entity::Address.path(client)).pluck('id') - end - - def update_remote(address) - rate_limiter.run { api.edit(Entity::Address.new(address).path, data(address)) } - end - - def create_remote(address) - response = rate_limiter.run { api.add(Entity::Address.path(client), data(address)) } - address.update_column(:invoicing_key, response.fetch('id')) - end - - def data(address) - Entity::Address.new(address).to_hash - end - - def reset_invoicing_keys(address, invoicing_key = nil) - address.update_column(:invoicing_key, invoicing_key) - end - - def key(address) - address.invoicing_key if key_exists_remotely?(address) - end - - def key_exists_remotely?(address) - address.invoicing_key.present? && remote_keys.map(&:to_s).include?(address.invoicing_key) - end - - def api - Invoicing::SmallInvoice::Api.instance - end - end - end -end diff --git a/app/domain/invoicing/small_invoice/api.rb b/app/domain/invoicing/small_invoice/api.rb index c920bc2e8..92c85aa9d 100644 --- a/app/domain/invoicing/small_invoice/api.rb +++ b/app/domain/invoicing/small_invoice/api.rb @@ -10,121 +10,58 @@ module SmallInvoice class Api include Singleton + ENDPOINTS = %w(invoice invoice/pdf client).freeze HTTP_TIMEOUT = 300 # seconds - LIST_PAGES_LIMIT = 100 - LIST_ENTRIES = 200 # the v2 api allows max 200 entries per page - def list(path, **params) - # The v2 api returns max 200 entries per query, so we loop through all pages and collect the result. - (0..LIST_PAGES_LIMIT).each_with_object([]) do |index, result| - response = get_json(path, **params.reverse_merge(limit: LIST_ENTRIES, offset: LIST_ENTRIES * index)) - result.append(*response['items']) - - return result unless response.dig('pagination', 'next') - end + def list(endpoint) + response = get_json(endpoint, :list) + response['items'] end - def get(path, **params) - response = get_json(path, **params) - response.fetch('item') + def get(endpoint, id) + response = get_json(endpoint, :get, id: id) + response['item'] end - def add(path, data) - response = post_json(path, **data) - response.fetch('item') + def add(endpoint, data) + response = post_request(endpoint, :add, data) + response['id'] end - def edit(path, data) - put_json(path, **data) + def edit(endpoint, id, data) + post_request(endpoint, :edit, data, id: id) nil end - def delete(path) - delete_request(path) + def delete(endpoint, id) + post_request(endpoint, :delete, nil, id: id) nil end - def get_raw(path, auth: true, **params) - get_request(path, auth:, **params).body + def get_raw(endpoint, action, id) + get_request(endpoint, action, id: id).body end private - def access_token - # fetch a new token if we have none yet or if the existing one is expired - @access_token, @expires_at = get_access_token unless @expires_at&.>(Time.zone.now) - @access_token - end - - # Get a new access token from the smallinvoice api. - # Returns an array with the access_token and the expiration time of this token. - def get_access_token - timestamp = Time.zone.now - - response = post_json( - 'auth/access-tokens', - auth: false, - grant_type: 'client_credentials', - client_id: settings.client_id, - client_secret: settings.client_secret, - scope: 'invoice contact' - ) - - response.fetch_values('access_token', 'expires_in').then do |token, expires_in| - [token, timestamp + expires_in] - end - end - - def get_json(path, auth: true, **params) - response = get_request(path, auth:, **params) + def get_json(endpoint, action, **params) + response = get_request(endpoint, action, **params) handle_json_response(response) end - def get_request(path, auth: true, **params) - url = build_url(path, **params) - request = Net::HTTP::Get.new(url.request_uri) - request['Authorization'] = "Bearer #{access_token}" if auth - + def get_request(endpoint, action, **params) + url = uri(endpoint, action, **params) + request = Net::HTTP::Get.new(url.path) http(url).request(request) end - def post_json(path, auth: true, **payload) - response = post_request(path, payload.to_json, auth:) - handle_json_response(response) - end - - def post_request(path, data, auth: true) - url = build_url(path) - request = Net::HTTP::Post.new(url, - 'Content-Type' => 'application/json') - request['Authorization'] = "Bearer #{access_token}" if auth - request.body = data - - http(url).request(request) - end + def post_request(endpoint, action, data, **params) + url = uri(endpoint, action, **params) + request = Net::HTTP::Post.new(url.path) + request.set_form_data(data ? { data: data.to_json } : {}) - def put_json(path, auth: true, **payload) - response = put_request(path, payload.to_json, auth:) - handle_json_response(response) - end - - def put_request(path, data, auth: true) - url = build_url(path) - request = Net::HTTP::Put.new(url, - 'Content-Type' => 'application/json') - request['Authorization'] = "Bearer #{access_token}" if auth - request.body = data - - http(url).request(request) - end - - def delete_request(path, auth: true) - url = build_url(path) - request = Net::HTTP::Delete.new(url, - 'Content-Type' => 'application/json') - request['Authorization'] = "Bearer #{access_token}" if auth - - http(url).request(request) + response = http(url).request(request) + handle_json_response(response, data) end def http(url) @@ -134,34 +71,25 @@ def http(url) end end - def build_url(path, **params) - url = [settings.url, path].join('/') - URI.parse(url).tap do |url| - url.query = URI.encode_www_form(params) if params.present? - end - end - - def handle_json_response(response) - handle_error(response) unless response.is_a? Net::HTTPSuccess - - return {} if response.body.blank? + def uri(endpoint, action, **params) + fail(ArgumentError, "Unknown endpoint #{endpoint}") unless ENDPOINTS.include?(endpoint.to_s) - parse_json_response(response) + params[:token] = Settings.small_invoice.api_token + args = params.collect { |k, v| "#{k}/#{v}" }.join('/') + URI("#{Settings.small_invoice.url}/#{endpoint}/#{action}/#{args}") end - def handle_error(response) - payload = parse_json_response(response) - raise Invoicing::Error.new(response.message, response.code, payload) - end + def handle_json_response(response, data = nil) + return {} if response.body.blank? - def parse_json_response(response) - JSON.parse(response.body) + json = JSON.parse(response.body) + if json['error'] + fail Invoicing::Error.new(json['errormessage'], json['errorcode'], data) + else + json + end rescue JSON::ParserError - raise Invoicing::Error.new('JSON::ParserError', response.code, response.body) - end - - def settings - Settings.small_invoice + fail Invoicing::Error.new(response.body, response.code, data) end end end diff --git a/app/domain/invoicing/small_invoice/client_sync.rb b/app/domain/invoicing/small_invoice/client_sync.rb index 123946c15..daaa325ae 100644 --- a/app/domain/invoicing/small_invoice/client_sync.rb +++ b/app/domain/invoicing/small_invoice/client_sync.rb @@ -12,23 +12,19 @@ class ClientSync class << self def perform remote_keys = fetch_remote_keys - failed = [] ::Client.includes(:work_item, :contacts, :billing_addresses).find_each do |client| if client.billing_addresses.present? # required by small invoice begin new(client, remote_keys).sync rescue StandardError => e - failed << client.id notify_sync_error(e, client) end end end - Rails.logger.error "Failed Client Syncs: #{failed.inspect}" if failed.any? end def fetch_remote_keys - path = Invoicing::SmallInvoice::Entity::Contact.path - Invoicing::SmallInvoice::Api.instance.list(path).each_with_object({}) do |client, hash| + SmallInvoice::Api.instance.list(:client).each_with_object({}) do |client, hash| hash[client['number']] = client['id'] end end @@ -65,7 +61,6 @@ def record_to_params(record, prefix = 'client') delegate :notify_sync_error, to: 'self.class' attr_reader :client, :remote_keys - class_attribute :rate_limiter self.rate_limiter = RateLimiter.new(Settings.small_invoice.request_rate) @@ -75,14 +70,32 @@ def initialize(client, remote_keys = nil) end def sync - key ? update_remote : create_remote - - ContactSync.new(client).sync - AddressSync.new(client).sync + if key + update_remote_with_timeouts + else + create_remote + set_association_keys_from_remote + end end private + def update_remote_with_timeouts + begin + update_remote + set_association_keys_from_remote + rescue Invoicing::Error => e + if e.code.to_s == '504' + # request is supposed to terminate eventually in the case of a gateway timeout, + # so schedule #set_association_keys for later + delay(run_at: 30.minutes.from_now).set_association_keys_from_remote + nil + else + raise + end + end + end + def update_remote if client.invoicing_key != key # Conflicting datasets in ptime <=> smallinvoice. We need to update the invoicing_key @@ -90,7 +103,7 @@ def update_remote # otherwise sync will abort because of conflicts. reset_invoicing_keys(key) end - rate_limiter.run { api.edit(Entity::Contact.new(client).path, data) } + rate_limiter.run { api.edit(:client, key, data) } end def create_remote @@ -99,17 +112,22 @@ def create_remote # executing the add action to avoid 15016 "no rights / not found" errors. reset_invoicing_keys - response = rate_limiter.run { api.add(Entity::Contact.path, data) } - client.update_column(:invoicing_key, response.fetch('id')) - client.billing_addresses.first.update_column(:invoicing_key, response.fetch('main_address_id')) + key = rate_limiter.run { api.add(:client, data) } + client.update_column(:invoicing_key, key) end def data - Entity::Contact.new(client).to_hash + Invoicing::SmallInvoice::Entity::Client.new(client).to_hash + end + + def set_association_keys_from_remote + remote = fetch_remote(client.invoicing_key) + set_association_keys(remote) if remote + nil end def fetch_remote(key) - rate_limiter.run { api.get(Entity::Contact.path(invoicing_key: key)) } + rate_limiter.run { api.get(:client, key) } rescue Invoicing::Error => e raise unless e.message == 'No Objects or too many found' @@ -117,6 +135,36 @@ def fetch_remote(key) nil end + def set_association_keys(remote) + set_association_key(Invoicing::SmallInvoice::Entity::Address, + client.billing_addresses, + remote.fetch('addresses', [])) + set_association_key(Invoicing::SmallInvoice::Entity::Contact, + client.contacts, + remote.fetch('contacts', [])) + end + + def set_association_key(entity, list, remote_list) + list.each do |item| + local_item = entity.new(item) + remote_keys = remote_list.select { |h| local_item == h }.map { |h| h['id'].to_s.presence }.compact + next if remote_keys.blank? || remote_keys.include?(item.invoicing_key) + + local_keys = list.model.where(invoicing_key: remote_keys).pluck(:invoicing_key) + new_remote_keys = remote_keys - local_keys + if new_remote_keys.blank? + notify_sync_error(Invoicing::Error.new('Unable to sync from remote, ' \ + 'record with invoicing_key already exists', + nil, + local_item: item.id, + invoicing_keys: remote_keys, + type: entity.name)) + else + item.update_column(:invoicing_key, new_remote_keys.first) + end + end + end + def reset_invoicing_keys(client_invoicing_key = nil) client.update_column(:invoicing_key, client_invoicing_key) client.billing_addresses.update_all(invoicing_key: nil) @@ -137,7 +185,7 @@ def key_exists_remotely? end def api - Invoicing::SmallInvoice::Api.instance + Api.instance end end end diff --git a/app/domain/invoicing/small_invoice/contact_sync.rb b/app/domain/invoicing/small_invoice/contact_sync.rb deleted file mode 100644 index d3bbaa17a..000000000 --- a/app/domain/invoicing/small_invoice/contact_sync.rb +++ /dev/null @@ -1,111 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module Invoicing - module SmallInvoice - # One-way sync from PuzzleTime to Small Invoice for clients, contacts and billing addresses. - class ContactSync - class << self - def notify_sync_error(error, contact = nil) - parameters = contact.present? ? record_to_params(contact) : {} - parameters[:code] = error.code if error.respond_to?(:code) - parameters[:data] = error.data if error.respond_to?(:data) - Airbrake.notify(error, parameters) if airbrake? - Raven.capture_exception(error, extra: parameters) if sentry? - end - - private - - def airbrake? - ENV['RAILS_AIRBRAKE_HOST'].present? - end - - def sentry? - ENV['SENTRY_DSN'].present? - end - - def record_to_params(record, prefix = 'billing_address') - { - "#{prefix}_id" => record.id, - "#{prefix}_invoicing_key" => record.invoicing_key, - "#{prefix}_shortname" => record.try(:shortname), - "#{prefix}_label" => record.try(:label) || record.to_s, - "#{prefix}_errors" => record.errors.messages, - "#{prefix}_changes" => record.changes - } - end - end - - delegate :notify_sync_error, to: 'self.class' - attr_reader :client, :remote_keys - - class_attribute :rate_limiter - self.rate_limiter = RateLimiter.new(Settings.small_invoice.request_rate) - - def initialize(client, remote_keys = nil) - @client = client - @remote_keys = remote_keys || fetch_remote_keys - end - - def sync - failed = [] - ::Contact.includes(:client).where(client_id: client.id).find_each do |contact| - key(contact) ? update_remote(contact) : create_remote(contact) - rescue StandardError => e - failed << contact.id - notify_sync_error(e, contact) - end - Rails.logger.error "Failed Contact Syncs: #{failed.inspect}" if failed.any? - end - - private - - def fetch_remote_keys - api.list(Entity::Person.path(client)).pluck('id') - end - - def update_remote(contact) - if contact.invoicing_key != key(contact) - # Conflicting datasets in ptime <=> smallinvoice. We need to update the invoicing_key - # of the client in ptime and clear the invoicing_keys of the addresses and contacts, - # otherwise sync will abort because of conflicts. - reset_invoicing_keys(contact, key(contact)) - end - rate_limiter.run { api.edit(Entity::Person.new(contact).path, data(contact)) } - end - - def create_remote(contact) - # Local clients may have an invoice key that does't exist in smallinvoice (e.g. when - # using a productive dump on ptime integration). So reset the invoicing keys before - # executing the add action to avoid 15016 "no rights / not found" errors. - reset_invoicing_keys(contact) - response = rate_limiter.run { api.add(Entity::Person.path(client), data(contact)) } - contact.update_column(:invoicing_key, response.fetch('id')) - end - - def data(contact) - Entity::Person.new(contact).to_hash - end - - def reset_invoicing_keys(contact, invoicing_key = nil) - contact.update_column(:invoicing_key, invoicing_key) - end - - def key(contact) - contact.invoicing_key if key_exists_remotely?(contact) - end - - def key_exists_remotely?(contact) - contact.invoicing_key.present? && remote_keys.map(&:to_s).include?(contact.invoicing_key) - end - - def api - Invoicing::SmallInvoice::Api.instance - end - end - end -end diff --git a/app/domain/invoicing/small_invoice/entity.rb b/app/domain/invoicing/small_invoice/entity.rb deleted file mode 100644 index 2baf1ee9c..000000000 --- a/app/domain/invoicing/small_invoice/entity.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module Invoicing - module SmallInvoice - module Entity; end - end -end diff --git a/app/domain/invoicing/small_invoice/entity/address.rb b/app/domain/invoicing/small_invoice/entity/address.rb index 89563f778..520bb9654 100644 --- a/app/domain/invoicing/small_invoice/entity/address.rb +++ b/app/domain/invoicing/small_invoice/entity/address.rb @@ -9,16 +9,6 @@ module Invoicing module SmallInvoice module Entity class Address < Base - ENDPOINT = 'addresses' - - def self.path(client, invoicing_key: nil) - [*Entity::Contact.new(client).path, ENDPOINT, invoicing_key].compact if client.persisted? - end - - def path - self.class.path(entry.client, invoicing_key: entry.invoicing_key) if persisted? - end - def to_hash street, street2 = entry.supplement? ? [entry.supplement, entry.street] : [entry.street, nil] with_id(street:, diff --git a/app/domain/invoicing/small_invoice/entity/base.rb b/app/domain/invoicing/small_invoice/entity/base.rb index 7b270e4b2..a2d6a9c09 100644 --- a/app/domain/invoicing/small_invoice/entity/base.rb +++ b/app/domain/invoicing/small_invoice/entity/base.rb @@ -41,10 +41,6 @@ def stringify(hash) memo[key.to_s] = value.to_s.strip end end - - def persisted? - entry.invoicing_key.present? - end end end end diff --git a/app/domain/invoicing/small_invoice/entity/client.rb b/app/domain/invoicing/small_invoice/entity/client.rb new file mode 100644 index 000000000..6e89fe321 --- /dev/null +++ b/app/domain/invoicing/small_invoice/entity/client.rb @@ -0,0 +1,34 @@ +# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of +# PuzzleTime and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/puzzle/puzzletime. + +module Invoicing + module SmallInvoice + module Entity + class Client < Base + def to_hash + { + number: entry.shortname, + name: entry.name, + type: constant(:client_type), + language: constant(:language), + einvoice_account_id: entry.e_bill_account_key, + addresses: entry.billing_addresses.list.collect.with_index do |a, i| + Address.new(a).to_hash.update(primary: i.zero?) + end, + contacts: entry.contacts.list.collect.with_index do |c, i| + # Set primary on the first contact to ensure we always have a + # primary contact for this client. + # Clients can only have one primary contact, + # setting more than one contact to primary will overwrite the + # existing one. + # See #20498 + Entity::Contact.new(c).to_hash.update(primary: i.zero?) + end + } + end + end + end + end +end diff --git a/app/domain/invoicing/small_invoice/entity/contact.rb b/app/domain/invoicing/small_invoice/entity/contact.rb index b60a2ae09..dccec7b3d 100644 --- a/app/domain/invoicing/small_invoice/entity/contact.rb +++ b/app/domain/invoicing/small_invoice/entity/contact.rb @@ -9,27 +9,12 @@ module Invoicing module SmallInvoice module Entity class Contact < Base - ENDPOINT = 'contacts' - - def self.path(invoicing_key: nil) - [ENDPOINT, invoicing_key].compact - end - - def path - self.class.path(invoicing_key: entry.invoicing_key) - end - def to_hash - { - number: entry.shortname, - relation: ['CL'], # TODO: move to config/settings.yml:small_invoice/constants - type: 'C', # TODO: move to config/settings.yml:small_invoice/constants - name: entry.name, - communication_language: constant(:language), - ebill_account_id: entry.e_bill_account_key, - - main_address: Entity::Address.new(entry.billing_addresses.first).to_hash - } + with_id(surname: entry.lastname, + name: entry.firstname, + email: entry.email, + phone: entry.phone, + gender: constant(:gender_id)) end end end diff --git a/app/domain/invoicing/small_invoice/entity/invoice.rb b/app/domain/invoicing/small_invoice/entity/invoice.rb index 3d41e05cf..cb759250e 100644 --- a/app/domain/invoicing/small_invoice/entity/invoice.rb +++ b/app/domain/invoicing/small_invoice/entity/invoice.rb @@ -9,8 +9,6 @@ module Invoicing module SmallInvoice module Entity class Invoice < Base - ENDPOINT = %w[receivables invoices].freeze - attr_reader :positions def initialize(invoice, positions) @@ -18,45 +16,32 @@ def initialize(invoice, positions) @positions = positions end - def self.path(invoicing_key: nil) - [*ENDPOINT, invoicing_key].compact - end - - def path - self.class.path(invoicing_key: entry.invoicing_key) - end - - def pdf_path - [*path, 'pdf'] - end - def to_hash { - number: entry.reference, - contact_id: Integer(entry.billing_address.client.invoicing_key), - contact_address_id: Integer(entry.billing_address.invoicing_key), - contact_person_id: entry.billing_address.contact.try(:invoicing_key)&.to_i, - date: entry.billing_date, - due: entry.due_date, - period: entry.period.to_s, - currency: Settings.defaults.currency, - vat_included: constant(:vat_included), - language: constant(:language), - - positions: positions.collect do |p| - Entity::Position.new(p).to_hash - end, - - texts: [ - { - status: 'D', # TODO: do we need other states? - title: entry.title, - conditions:, - introduction: - } - ] - - # totalamount: entry.total_amount.round(2), + number: entry.reference, + client_id: entry.billing_address.client.invoicing_key, + client_address_id: entry.billing_address.invoicing_key, + client_contact_id: entry.billing_address.contact.try(:invoicing_key), + currency: Settings.defaults.currency, + title: entry.title, + period: entry.period.to_s, + date: entry.billing_date, + due: entry.due_date, + account_id: constant(:account_id), + esr: bool_constant(:esr), + esr_singlepage: bool_constant(:esr_singlepage), + lsvplus: bool_constant(:lsvplus), + dd: bool_constant(:debit_direct), + conditions: conditions, + introduction: introduction, + language: constant(:language), + paypal: bool_constant(:paypal), + paypal_url: constant(:paypay_url), + vat_included: constant(:vat_included), + totalamount: entry.total_amount.round(2), + positions: positions.collect do |p| + Invoicing::SmallInvoice::Entity::Position.new(p).to_hash + end } end diff --git a/app/domain/invoicing/small_invoice/entity/person.rb b/app/domain/invoicing/small_invoice/entity/person.rb deleted file mode 100644 index dc96e403a..000000000 --- a/app/domain/invoicing/small_invoice/entity/person.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2006-2017, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module Invoicing - module SmallInvoice - module Entity - class Person < Base - ENDPOINT = 'people' - - def self.path(client, invoicing_key: nil) - [*Entity::Contact.new(client).path, ENDPOINT, invoicing_key].compact if client.persisted? - end - - def path - self.class.path(entry.client, invoicing_key: entry.invoicing_key) if persisted? - end - - def to_hash - with_id(surname: entry.lastname, - name: entry.firstname, - email: entry.email, - phone: entry.phone, - gender: 'F') - end - end - end - end -end diff --git a/app/domain/invoicing/small_invoice/entity/position.rb b/app/domain/invoicing/small_invoice/entity/position.rb index 2e17cad80..d0c1427a8 100644 --- a/app/domain/invoicing/small_invoice/entity/position.rb +++ b/app/domain/invoicing/small_invoice/entity/position.rb @@ -11,15 +11,14 @@ module Entity class Position < Base def to_hash { - type: 'N', - catalog_type: constant(:position_type), + type: constant(:position_type_id), number: nil, name: entry.name, description: nil, - price: post.offered_rate.round(2).to_f, - vat: Settings.defaults.vat, - amount: entry.total_hours.round(2).to_f, - unit_id: constant(:unit_id) + cost: post.offered_rate.try(:round, 2), + unit: constant(:unit_id), + amount: entry.total_hours.round(2), + vat: Settings.defaults.vat } end diff --git a/app/domain/invoicing/small_invoice/interface.rb b/app/domain/invoicing/small_invoice/interface.rb index 936eaa8f6..559fb1c34 100644 --- a/app/domain/invoicing/small_invoice/interface.rb +++ b/app/domain/invoicing/small_invoice/interface.rb @@ -21,7 +21,7 @@ def sync_invoice(invoice) def delete_invoice(invoice) return unless invoice.invoicing_key? - Invoicing::SmallInvoice::Api.instance.delete(Entity::Invoice.path(invoicing_key: invoice.invoicing_key)) + Api.instance.delete(:invoice, invoice.invoicing_key) end def sync_all @@ -30,7 +30,7 @@ def sync_all end def get_pdf(invoice) - Invoicing::SmallInvoice::Api.instance.get_raw(Entity::Invoice.new(invoice, nil).pdf_path) + Api.instance.get_raw('invoice', :pdf, invoice.invoicing_key) end end end diff --git a/app/domain/invoicing/small_invoice/invoice_store.rb b/app/domain/invoicing/small_invoice/invoice_store.rb index bb3490030..0aed3bab9 100644 --- a/app/domain/invoicing/small_invoice/invoice_store.rb +++ b/app/domain/invoicing/small_invoice/invoice_store.rb @@ -19,13 +19,12 @@ def initialize(invoice) def save(positions) assert_remote_client_exists - entity = Entity::Invoice.new(invoice, positions) + data = Invoicing::SmallInvoice::Entity::Invoice.new(invoice, positions).to_hash if invoice.invoicing_key? - api.edit(entity.path, entity.to_hash) + api.edit(:invoice, invoice.invoicing_key, data) invoice.invoicing_key else - result = api.add(entity.class.path, entity.to_hash) - result['id'] + api.add(:invoice, data) end end @@ -43,7 +42,7 @@ def assert_remote_client_exists end def api - Invoicing::SmallInvoice::Api.instance + Api.instance end end end diff --git a/app/domain/invoicing/small_invoice/invoice_sync.rb b/app/domain/invoicing/small_invoice/invoice_sync.rb index 5802b0f25..2067f7787 100644 --- a/app/domain/invoicing/small_invoice/invoice_sync.rb +++ b/app/domain/invoicing/small_invoice/invoice_sync.rb @@ -9,38 +9,28 @@ module Invoicing module SmallInvoice # One-way sync of invoices from Small Invoice to PuzzleTime class InvoiceSync - # status (string): status of invoice, possible values: - # DR - draft, S - sent, P - paid, PP - partially paid, R1 - 1st reminder, R2 - 2nd reminder, R3 - 3rd reminder, - # R - reminder, DC - debt collection, C - cancelled, D - deleted (but still visible) , - STATUS = { - 'DR' => 'draft', - 'S' => 'sent', - 'P' => 'paid', - 'PP' => 'partially_paid', - 'R1' => 'sent', - 'R2' => 'sent', - 'R3' => 'sent', - 'R' => 'sent', - 'DC' => 'dept_collection', - 'C' => 'cancelled', - 'D' => 'deleted' - }.freeze + STATUS = { 1 => 'sent', # sent / open + 2 => 'paid', # paid + 3 => 'sent', # 1st reminder + 4 => 'sent', # 2nd reminder + 5 => 'sent', # 3rd reminder + 6 => 'cancelled', # cancelled + 7 => 'draft', # draft + 11 => 'partially_paid', # partially paid + 12 => 'sent', # reminder + 99 => 'deleted' }.freeze # deleted attr_reader :invoice - class_attribute :rate_limiter self.rate_limiter = RateLimiter.new(Settings.small_invoice.request_rate) class << self def sync_unpaid - failed = [] unpaid_invoices.find_each do |invoice| new(invoice).sync rescue StandardError => e - failed << invoice.id notify_sync_error(e, invoice) end - Rails.logger.error "Failed Invoice Syncs: #{failed.inspect}" if failed.any? end private @@ -84,11 +74,7 @@ def initialize(invoice) # Fetch an invoice from remote and update the local values def sync - return unless invoice.invoicing_key - - item = rate_limiter.run do - api.get(Entity::Invoice.path(invoicing_key: invoice.invoicing_key), with: 'positions') - end + item = rate_limiter.run { api.get(:invoice, invoice.invoicing_key) } sync_remote(item) rescue Invoicing::Error => e if e.code == 15_016 # no rights / not found @@ -128,18 +114,21 @@ def delete_invoice(force = false) def total_hours(item) item['positions'].select do |p| - p['catalog_type'] == Settings.small_invoice.constants.position_type && - p['unit_id'] == Settings.small_invoice.constants.unit_id - end.pluck('amount').sum + p['type'] == Settings.small_invoice.constants.position_type_id && + p['unit'] == Settings.small_invoice.constants.unit_id + end.collect do |p| + p['amount'] + end.sum end # item['totalamount'] always includes vat # item['vat_included'] tells whether position totals already include vat or not. def total_amount_without_vat(item) - item['positions'].select { |p| p['price'] }.sum do |p| - total = p['price'] * p['amount'] + vat_included = !item['vat_included'].zero? + item['positions'].select { |p| p['cost'] }.map do |p| + total = p['cost'] * p['amount'] total -= position_discount(p, total) - total -= position_included_vat(p, total) if item['vat_included'] + total -= position_included_vat(p, total) if vat_included total end end @@ -161,7 +150,7 @@ def position_included_vat(p, total) end def api - Invoicing::SmallInvoice::Api.instance + Api.instance end end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index c07dbbc73..6418a62eb 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -26,8 +26,8 @@ # grouping :integer default("accounting_posts"), not null # -class Invoice < ApplicationRecord - STATUSES = %w[draft sent paid partially_paid dept_collection cancelled deleted unknown].freeze +class Invoice < ActiveRecord::Base + STATUSES = %w(draft sent paid partially_paid cancelled deleted unknown).freeze enum grouping: { 'accounting_posts' => 0, 'employees' => 1, 'manual' => 2 } @@ -51,12 +51,12 @@ class Invoice < ApplicationRecord before_validation :generate_reference, on: :create before_validation :generate_due_date before_validation :update_totals - before_save :save_remote_invoice, if: -> { Invoicing.instance.present? } - before_save :assign_worktimes before_create :lock_client_invoice_number after_create :update_client_invoice_number - after_destroy :delete_remote_invoice, if: -> { Invoicing.instance.present? } after_save :update_order_billing_address + before_save :save_remote_invoice, if: -> { Invoicing.instance.present? } + before_save :assign_worktimes + after_destroy :delete_remote_invoice, if: -> { Invoicing.instance.present? } protect_if :paid?, 'Bezahlte Rechnungen können nicht gelöscht werden.' protect_if :order_closed?, 'Rechnungen von geschlossenen Aufträgen können nicht gelöscht werden.' @@ -225,11 +225,7 @@ def save_remote_invoice self.invoicing_key = Invoicing.instance.save_invoice(self, positions) rescue Invoicing::Error => e errors.add(:base, "Fehler im Invoicing Service: #{e.message}") - Rails.logger.error <<~ERROR - #{e.class.name}: #{e.message} - #{e.data.inspect} - #{e.backtrace.join("\n")} - ERROR + Rails.logger.error(e.class.name + ': ' + e.message + "\n" + e.backtrace.join("\n")) throw :abort end diff --git a/config/locales/models.de-CH.yml b/config/locales/models.de-CH.yml index 6d5f9b5b7..b76d730ad 100644 --- a/config/locales/models.de-CH.yml +++ b/config/locales/models.de-CH.yml @@ -293,7 +293,6 @@ de-CH: sent: Gesendet paid: Bezahlt partially_paid: Teilbezahlt - dept_collection: Betreibung deleted: Gelöscht cancelled: Storniert unknown: Unbekannt diff --git a/config/locales/models.de-DE.yml b/config/locales/models.de-DE.yml index 10f4abc1c..a86972054 100644 --- a/config/locales/models.de-DE.yml +++ b/config/locales/models.de-DE.yml @@ -256,7 +256,6 @@ de-DE: sent: Gesendet paid: Bezahlt partially_paid: Teilbezahlt - dept_collection: Betreibung deleted: Gelöscht cancelled: Storniert unknown: Unbekannt diff --git a/config/settings.yml b/config/settings.yml index cfeb5cd0e..13c979620 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -83,9 +83,8 @@ highrise: api_token: <%= ENV['RAILS_HIGHRISE_TOKEN'] %> small_invoice: - client_id: <%= ENV['RAILS_SMALL_INVOICE_CLIENT_ID'] %> - client_secret: <%= ENV['RAILS_SMALL_INVOICE_CLIENT_SECRET'] %> - url: https://api.smallinvoice.com/v2 + url: https://api.smallinvoice.com + api_token: <%= ENV['RAILS_SMALL_INVOICE_TOKEN'] %> request_rate: <%= ENV['RAILS_SMALL_INVOICE_REQUEST_RATE'] || 1 %> constants: account_id: 0 # none @@ -99,7 +98,6 @@ small_invoice: vat_included: false client_type: 1 # company position_type_id: 1 # service - position_type: 'S' # service unit_id: 1 # hours gender_id: 2 # female diff --git a/test/domain/invoicing/small_invoice/address_sync_test.rb b/test/domain/invoicing/small_invoice/address_sync_test.rb deleted file mode 100644 index b3a8442ca..000000000 --- a/test/domain/invoicing/small_invoice/address_sync_test.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2006-2020, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -require 'test_helper' -module Invoicing - module SmallInvoice - class AddressSyncTest < ActiveSupport::TestCase - include SmallInvoiceTestHelper - - test '#sync without existing client creates address' do - add_address = stub_add_entity(:addresses, body: address_json, response: address_json_response) - - subject.sync - - assert_requested(add_address) - end - - test '#sync with existing client but new address creates it' do - client.update_column(:invoicing_key, 1234) - edit_address = stub_add_entity(:addresses, client:, body: address_json, response: address_json_response) - - subject.sync - - assert_requested(edit_address) - end - - test '#sync with existing client and address edits it' do - client.update_column(:invoicing_key, 1234) - billing_address.update_column(:invoicing_key, 1) - edit_address = stub_edit_entity(:addresses, client:, key: 1, body: address_id_json, - response: address_json_response) - - subject_with_address.sync - - assert_requested(edit_address) - end - - private - - def described_class - Invoicing::SmallInvoice::AddressSync - end - - def subject - described_class.new(clients(:puzzle), []) - end - - def subject_with_address - described_class.new(clients(:puzzle), [1]) - end - - def client - clients(:puzzle) - end - - def billing_address - billing_addresses(:puzzle) - end - - def address_json - '{"street":"Eigerplatz 4","street2":null,"postcode":"3007","city":"Bern","country":"CH"}' - end - - def address_id_json - '{"street":"Eigerplatz 4","street2":null,"postcode":"3007","city":"Bern","country":"CH","id":"1"}' - end - - def address_json_response - %({"item":#{address_id_json}}) - end - end - end -end diff --git a/test/domain/invoicing/small_invoice/api_test.rb b/test/domain/invoicing/small_invoice/api_test.rb deleted file mode 100644 index c8c1b7c4c..000000000 --- a/test/domain/invoicing/small_invoice/api_test.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2006-2020, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -require 'test_helper' - -module Invoicing - module SmallInvoice - class ApiTest < ActiveSupport::TestCase - include SmallInvoiceTestHelper - - test '#list' do - id = id(:contacts) - path = path(:contacts) - get_contacts = stub_get_entity(:contacts) - - list = subject.list(path) - - assert_requested(get_contacts) - assert_instance_of Array, list - - contact = list.first - - assert_equal id, contact['id'] - end - - test '#get' do - id = id(:contacts) - path = path(:contacts, key: id) - get_contact = stub_get_entity(:contacts, key: id) - - contact = subject.get(path) - - assert_requested(get_contact) - assert_equal id, contact['id'] - end - - test '#add' do - id = id(:contacts) - path = path(:contacts) - add_contact = stub_add_entity(:contacts) - - contact = subject.add(path, new_contact) - - assert_requested(add_contact) - assert_equal id, contact['id'] - end - - test '#edit' do - path = path(:contacts) - edit_contact = stub_edit_entity(:contacts) - - assert_nil subject.edit(path, new_contact) - assert_requested(edit_contact) - end - - test '#delete' do - path = path(:contacts) - delete_contact = stub_delete_entity(:contacts) - - assert_nil subject.delete(path) - assert_requested(delete_contact) - end - - private - - def subject - Invoicing::SmallInvoice::Api.instance - end - end - end -end diff --git a/test/domain/invoicing/small_invoice/client_sync_test.rb b/test/domain/invoicing/small_invoice/client_sync_test.rb deleted file mode 100644 index e1634247f..000000000 --- a/test/domain/invoicing/small_invoice/client_sync_test.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2006-2020, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -require 'test_helper' - -module Invoicing - module SmallInvoice - class ClientSyncTest < ActiveSupport::TestCase - include SmallInvoiceTestHelper - - test '#sync' do - # updates contacts - get_contacts = stub_get_entity(:contacts) - add_contact = stub_add_entity(:contacts) - - stub_syncs - - subject.sync - - assert_requested(get_contacts) - assert_requested(add_contact) - end - - private - - def described_class - Invoicing::SmallInvoice::ClientSync - end - - def subject - described_class.new(clients(:puzzle)) - end - - def stub_syncs - contact_sync = mock - contact_sync.expects(:sync).once - Invoicing::SmallInvoice::ContactSync - .stubs(:new) - .returns(contact_sync) - - address_sync = mock - address_sync.expects(:sync).once - Invoicing::SmallInvoice::AddressSync - .stubs(:new) - .returns(address_sync) - end - end - end -end diff --git a/test/domain/invoicing/small_invoice/contact_sync_test.rb b/test/domain/invoicing/small_invoice/contact_sync_test.rb deleted file mode 100644 index af0ba6e80..000000000 --- a/test/domain/invoicing/small_invoice/contact_sync_test.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2006-2020, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -require 'test_helper' - -module Invoicing - module SmallInvoice - class ContactSyncTest < ActiveSupport::TestCase - include SmallInvoiceTestHelper - - test '#sync new client' do - # updates people - add_hans = stub_add_entity(:people, client:, body: hans_json) - add_andreas = stub_add_entity(:people, client:, body: andreas_json) - - subject.sync - - assert_requested(add_hans) - assert_requested(add_andreas) - end - - test '#sync existing client' do - client.update_column(:invoicing_key, 1234) - andreas.update_column(:invoicing_key, 2) - - # updates people - add_hans = stub_add_entity(:people, client:, body: hans_json) - edit_andreas = stub_edit_entity(:people, client:, key: 2, body: edit_andreas_json) - - subject_with_existing.sync - - assert_requested(add_hans) - assert_requested(edit_andreas) - end - - private - - def described_class - Invoicing::SmallInvoice::ContactSync - end - - def subject - described_class.new(client, []) - end - - def subject_with_existing - described_class.new(client, [2]) - end - - def client - clients(:puzzle) - end - - def andreas - contacts(:puzzle_rava) - end - - def hans_json - '{"surname":"Hauswart","name":"Hans","email":"hauswart@example.com","phone":null,"gender":"F"}' - end - - def andreas_json - '{"surname":"Rava","name":"Andreas","email":"rava@example.com","phone":null,"gender":"F"}' - end - - def edit_andreas_json - '{"surname":"Rava","name":"Andreas","email":"rava@example.com","phone":null,"gender":"F","id":"2"}' - end - end - end -end diff --git a/test/domain/invoicing/small_invoice/interface_test.rb b/test/domain/invoicing/small_invoice/interface_test.rb deleted file mode 100644 index e69be73d3..000000000 --- a/test/domain/invoicing/small_invoice/interface_test.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2006-2020, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -require 'test_helper' - -module Invoicing - module SmallInvoice - class InterfaceTest < ActiveSupport::TestCase - include SmallInvoiceTestHelper - - test '#save_invoice creates' do - Invoicing::SmallInvoice::InvoiceStore.any_instance.stubs(:save).returns(true) - - assert subject.save_invoice(invoice, []) - end - - test '#save_invoice updates' do - invoice.update_column(:invoicing_key, 1) - Invoicing::SmallInvoice::InvoiceStore.any_instance.stubs(:save).returns(true) - - assert subject.save_invoice(invoice, [1]) - end - - test '#sync_invoice' do - invoice.update_column(:invoicing_key, 1) - Invoicing::SmallInvoice::InvoiceSync.any_instance.stubs(:sync).returns(true) - - assert subject.sync_invoice(invoice) - end - - test '#sync_invoice without invoicing_key' do - Invoicing::SmallInvoice::InvoiceSync.any_instance.stubs(:sync).returns(true) - - assert_nil subject.sync_invoice(invoice) - end - - test '#delete_invoice with invoicing_key' do - invoice.update_column(:invoicing_key, 1) - stub_auth - delete_invoice = stub_delete_entity(:invoices, key: 1) - subject.delete_invoice(invoice) - - assert_requested(delete_invoice) - end - - test '#delete_invoice without invoicing_key' do - assert_nil subject.delete_invoice(invoice) - end - - test '#sync_all' do - Invoicing::SmallInvoice::ClientSync.expects(:perform).once - Invoicing::SmallInvoice::InvoiceSync.expects(:sync_unpaid).once - - subject.sync_all - end - - test '#get_pdf' do - invoice.update_column(:invoicing_key, 1) - stub_auth - get_pdf = stub_request(:get, "#{BASE_URL}/receivables/invoices/1/pdf") - - subject.get_pdf(invoice) - - assert_requested(get_pdf) - end - - private - - def described_class - Invoicing::SmallInvoice::Interface - end - - def subject - described_class.new - end - - def invoice - invoices(:webauftritt_may) - end - end - end -end diff --git a/test/domain/invoicing/small_invoice/invoice_store_test.rb b/test/domain/invoicing/small_invoice/invoice_store_test.rb deleted file mode 100644 index 761f9a25d..000000000 --- a/test/domain/invoicing/small_invoice/invoice_store_test.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2006-2020, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -require 'test_helper' - -module Invoicing - module SmallInvoice - class InvoiceStoreTest < ActiveSupport::TestCase - include SmallInvoiceTestHelper - - setup do - billing_address.update_column(:invoicing_key, 2) - contact.update_column(:invoicing_key, 1) - client.update_column(:invoicing_key, 1) - end - - test '#save creates new invoices' do - add_invoice = stub_add_entity(:invoices, body: invoice_json) - - invoicing_key = subject.save([manual_position]) - - assert_requested(add_invoice) - assert_predicate invoicing_key, :present? - end - - test '#save edits existing invoices' do - invoice.update_column(:invoicing_key, 1) - - edit_invoice = stub_edit_entity(:invoices, key: 1, body: invoice_json) - - invoicing_key = subject.save([manual_position]) - - assert_requested(edit_invoice) - assert_equal(invoice.invoicing_key, invoicing_key) - end - - private - - def described_class - Invoicing::SmallInvoice::InvoiceStore - end - - def subject - described_class.new(invoice) - end - - def invoice - invoices(:webauftritt_may) - end - - def billing_address - invoice.billing_address - end - - def contact - billing_address.contact - end - - def client - billing_address.client - end - - def manual_position - Invoicing::Position.new(AccountingPost.new(offered_rate: 1), 1, 'Manuell') - end - - def invoice_json - JSON.parse('{ - "number":"STOPWEBD10001", - "contact_id":1, - "contact_address_id":2, - "contact_person_id":1, - "date":"2015-06-15", - "due":"2015-07-14", - "period":"01.12.2006 - 31.12.2006", - "currency":"CHF", - "vat_included":false, - "language":"de", - "positions":[ - { - "type":"N", - "catalog_type":"S", - "number":null, - "name":"Manuell", - "description":null, - "price":1.0, - "vat":7.7, - "amount":1.0, - "unit_id":1 - } - ], - "texts":[ - { - "status":"D", - "title":"Webauftritt gemäss Vertrag web1234", - "conditions":"Zahlbar innert 45 Tagen ab Rechnungsdatum.", - "introduction":"Besten Dank für Ihren Auftrag\\n\\nIhre Referenzinformationen:\\norder webauftritt 1234" - } - ] - }').to_json - end - end - end -end diff --git a/test/domain/invoicing/small_invoice/invoice_sync_test.rb b/test/domain/invoicing/small_invoice/invoice_sync_test.rb deleted file mode 100644 index 20c470467..000000000 --- a/test/domain/invoicing/small_invoice/invoice_sync_test.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2006-2020, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -require 'test_helper' - -module Invoicing - module SmallInvoice - class InvoiceSyncTest < ActiveSupport::TestCase - include SmallInvoiceTestHelper - - test '#sync' do - get_invoice = stub_get_entity( - :invoices, - params: '?with=positions', - key: 1, - response: invoice_json - ) - - subject.sync - - assert_requested(get_invoice) - end - - private - - def described_class - Invoicing::SmallInvoice::InvoiceSync - end - - def subject - described_class.new(invoice) - end - - def invoice - invoice = invoices(:webauftritt_may) - invoice.update_column(:invoicing_key, 1) - invoice - end - - def invoice_json - '{ - "item": { - "bank_account_id": null, - "cash_discount_date": null, - "cash_discount_rate": null, - "contact_address_id": 512553413, - "contact_id": 117463039, - "contact_person_id": 556068567, - "contact_prepage_address_id": null, - "created": "2015-11-26 09:18:15", - "currency": "CHF", - "date": "2015-11-26", - "discount_rate": 0.0, - "discount_type": "P", - "due": "2015-12-26", - "id": 699144547, - "isr_id": 112463152, - "isr_position": "A", - "isr_reference_number": "921736000000000000000001546", - "language": "de", - "layout_id": 297477064, - "notes": null, - "number": "BLSB-DIS-WEI-D2-0023", - "page_amount": 2, - "paid_date": null, - "payment_link_paypal": false, - "payment_link_paypal_url": null, - "payment_link_payrexx": false, - "payment_link_payrexx_url": null, - "payment_link_postfinance": false, - "payment_link_postfinance_url": null, - "payment_link_smartcommerce": false, - "payment_link_smartcommerce_url": null, - "period_from": null, - "period_text": "01.12.2014 - 31.12.2014", - "period_to": null, - "positions": [ - { - "amount": 86.58, - "catalog_type": "S", - "description": "", - "discount_rate": 0.0, - "discount_type": "P", - "name": "DIS Weiterentwicklung 2015", - "number": null, - "price": 128.4, - "show_only_total": false, - "total": 12006.22, - "type": "N", - "unit_id": 1, - "vat": 8.0 - } - ], - "signature_id": null, - "status": "S", - "total": 12006.2, - "total_paid": 0.0, - "vat_included": false - } - }' - end - end - end -end diff --git a/test/fixtures/files/small_invoice/addresses.json b/test/fixtures/files/small_invoice/addresses.json deleted file mode 100644 index 37c1a9e43..000000000 --- a/test/fixtures/files/small_invoice/addresses.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "items": [{"id": 1}], - "pagination": { - "page": 1, - "pages": 1, - "total": 200, - "first": "https://api.smallinvoice.com/v2/addresses?limit=200&offset=0", - "last": "https://api.smallinvoice.com/v2/addresses?limit=200&offset=0" - } -} diff --git a/test/fixtures/files/small_invoice/contact.json b/test/fixtures/files/small_invoice/contact.json deleted file mode 100644 index e0d151877..000000000 --- a/test/fixtures/files/small_invoice/contact.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "item": { - "id": 199493817, - "number": "CHOP", - "relation": [ - "CL" - ], - "type": "C", - "gender": null, - "gender_salutation_active": false, - "name": "/ch/open", - "name_addition": null, - "salutation": null, - "phone": null, - "fax": null, - "email": null, - "website": null, - "notes": null, - "communication_language": "de", - "communication_channel": "U", - "communication_newsletter": "A", - "currency": null, - "ebill_account_id": "41105678901234567", - "vat_identification": null, - "vat_rate": null, - "discount_rate": 0.0, - "discount_type": "P", - "payment_grace": null, - "hourly_rate": null, - "created": "2015-06-08 14:34:16", - "main_address_id": 583510867 - } -} diff --git a/test/fixtures/files/small_invoice/contacts.json b/test/fixtures/files/small_invoice/contacts.json deleted file mode 100644 index dc79518b4..000000000 --- a/test/fixtures/files/small_invoice/contacts.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "items": [ - { - "id": 199493817, - "number": "CHOP", - "relation": [ - "CL" - ], - "type": "C", - "gender": null, - "gender_salutation_active": false, - "name": "/ch/open", - "name_addition": null, - "salutation": null, - "phone": null, - "fax": null, - "email": null, - "website": null, - "notes": null, - "communication_language": "de", - "communication_channel": "U", - "communication_newsletter": "A", - "currency": null, - "ebill_account_id": "41105678901234567", - "vat_identification": null, - "vat_rate": null, - "discount_rate": 0.0, - "discount_type": "P", - "payment_grace": null, - "hourly_rate": null, - "created": "2015-06-08 14:34:16", - "main_address_id": 583510867 - } - ], - "pagination": { - "page": 1, - "pages": 1, - "total": 200, - "first": "https://api.smallinvoice.com/v2/contacts?limit=200&offset=0", - "last": "https://api.smallinvoice.com/v2/contacts?limit=200&offset=0" - } -} diff --git a/test/fixtures/files/small_invoice/invoice.json b/test/fixtures/files/small_invoice/invoice.json deleted file mode 100644 index 66beeeadf..000000000 --- a/test/fixtures/files/small_invoice/invoice.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "item": { - "id": 699144547, - "number": "BLSB-DIS-WEI-D2-0023", - "contact_id": 117463039, - "contact_address_id": 512553413, - "contact_prepage_address_id": null, - "contact_person_id": 556068567, - "date": "2015-11-26", - "due": "2015-12-26", - "period_from": null, - "period_to": null, - "period_text": "01.12.2014 - 31.12.2014", - "currency": "CHF", - "total": 12006.2, - "vat_included": false, - "discount_rate": 0.0, - "discount_type": "P", - "cash_discount_rate": null, - "cash_discount_date": null, - "total_paid": 0.0, - "paid_date": null, - "bank_account_id": null, - "isr_id": 112463152, - "isr_position": "A", - "isr_reference_number": "921736000000000000000001546", - "payment_link_paypal": false, - "payment_link_paypal_url": null, - "payment_link_postfinance": false, - "payment_link_postfinance_url": null, - "payment_link_payrexx": false, - "payment_link_payrexx_url": null, - "payment_link_smartcommerce": false, - "payment_link_smartcommerce_url": null, - "language": "de", - "signature_id": null, - "layout_id": 297477064, - "page_amount": 2, - "notes": null, - "status": "S", - "created": "2015-11-26 09:18:15" - } -} diff --git a/test/fixtures/files/small_invoice/invoices.json b/test/fixtures/files/small_invoice/invoices.json deleted file mode 100644 index 882607999..000000000 --- a/test/fixtures/files/small_invoice/invoices.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "items": [ - { - "id": 699144547, - "number": "BLSB-DIS-WEI-D2-0023", - "contact_id": 117463039, - "contact_address_id": 512553413, - "contact_prepage_address_id": null, - "contact_person_id": 556068567, - "date": "2015-11-26", - "due": "2015-12-26", - "period_from": null, - "period_to": null, - "period_text": "01.12.2014 - 31.12.2014", - "currency": "CHF", - "total": 12006.2, - "vat_included": false, - "discount_rate": 0.0, - "discount_type": "P", - "cash_discount_rate": null, - "cash_discount_date": null, - "total_paid": 0.0, - "paid_date": null, - "bank_account_id": null, - "isr_id": 112463152, - "isr_position": "A", - "isr_reference_number": "921736000000000000000001546", - "payment_link_paypal": false, - "payment_link_paypal_url": null, - "payment_link_postfinance": false, - "payment_link_postfinance_url": null, - "payment_link_payrexx": false, - "payment_link_payrexx_url": null, - "payment_link_smartcommerce": false, - "payment_link_smartcommerce_url": null, - "language": "de", - "signature_id": null, - "layout_id": 297477064, - "page_amount": 2, - "notes": null, - "status": "S", - "created": "2015-11-26 09:18:15" - } - ], - "pagination": { - "page": 1, - "pages": 1, - "total": 77, - "first": "https://api.smallinvoice.com/v2/receivables/invoices?limit=200&offset=0", - "last": "https://api.smallinvoice.com/v2/receivables/invoices?limit=200&offset=0" - } -} diff --git a/test/fixtures/files/small_invoice/people.json b/test/fixtures/files/small_invoice/people.json deleted file mode 100644 index 6cd3dfb8d..000000000 --- a/test/fixtures/files/small_invoice/people.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "items": [ - { - "id": 575533819, - "default": false, - "name": "Müller", - "surname": "Hans", - "gender": "F", - "email": null, - "phone": null, - "department": null, - "salutation": null, - "show_title": false, - "show_department": true, - "wants_newsletter": true - } - ], - "pagination": { - "page": 1, - "pages": 1, - "total": 1, - "first": "https://api.smallinvoice.com/v2/contacts/199493817/people?limit=200&offset=0", - "last": "https://api.smallinvoice.com/v2/contacts/199493817/people?limit=200&offset=0" - } -} diff --git a/test/fixtures/files/small_invoice/person.json b/test/fixtures/files/small_invoice/person.json deleted file mode 100644 index a113399c2..000000000 --- a/test/fixtures/files/small_invoice/person.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "item": { - "id": 575533819, - "default": false, - "name": "Müller", - "surname": "Hans", - "gender": "F", - "email": null, - "phone": null, - "department": null, - "salutation": null, - "show_title": false, - "show_department": true, - "wants_newsletter": true - } -} diff --git a/test/fixtures/files/small_invoice/positions.json b/test/fixtures/files/small_invoice/positions.json deleted file mode 100644 index e35c54d1d..000000000 --- a/test/fixtures/files/small_invoice/positions.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "items": [{"id": 1}], - "pagination": { - "page": 1, - "pages": 1, - "total": 200, - "first": "https://api.smallinvoice.com/v2/positions?limit=200&offset=0", - "last": "https://api.smallinvoice.com/v2/positions?limit=200&offset=0" - } -} diff --git a/test/support/small_invoice_test_helper.rb b/test/support/small_invoice_test_helper.rb deleted file mode 100644 index 06f2af592..000000000 --- a/test/support/small_invoice_test_helper.rb +++ /dev/null @@ -1,138 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2006-2020, Puzzle ITC GmbH. This file is part of -# PuzzleTime and licensed under the Affero General Public License version 3 -# or later. See the COPYING file at the top-level directory or at -# https://github.com/puzzle/puzzletime. - -module SmallInvoiceTestHelper - extend ActiveSupport::Concern - - included do - setup :stub_auth - end - - BASE_URL = 'https://api.smallinvoice.com/v2' - - def entity(name) - "Invoicing::SmallInvoice::Entity::#{name.to_s.singularize.classify}".constantize - end - - def stub_auth - stub_request(:post, "#{BASE_URL}/auth/access-tokens") - .to_return(status: 200, body: auth_body) - end - - def stub_get_entity(name, **kwargs) - args = kwargs.reverse_merge( - { - params: kwargs[:key] ? nil : '?limit=200&offset=0' - } - ) - stub_api_request(:get, name, **args) - end - - def stub_add_entity(name, **kwargs) - args = kwargs.reverse_merge( - { - body: JSON.generate(new_contact), - response: single_response(name) - } - ) - - stub_api_request(:post, name, **args) - end - - def stub_edit_entity(name, **kwargs) - args = { - body: JSON.generate(new_contact) - }.merge(kwargs) - - stub_api_request(:put, name, **args) - end - - def stub_delete_entity(name, **) - stub_api_request(:delete, name, **) - end - - def path(name, **kwargs) - key = kwargs[:key] - - if %i[people addresses].include?(name) - parent = kwargs[:parent] || default_client - return entity(name).path(parent, invoicing_key: key) if key - - entity(name).path(parent) - else - return entity(name).path(invoicing_key: key) if key - - entity(name).path - end - end - - def path_url(name, **) - path(name, **).join('/') - end - - private - - def stub_api_request(method, name, **kwargs) - key = kwargs[:key] - path = kwargs[:path] || path_url(name, **kwargs) - params = kwargs[:params] - url = kwargs[:url] || "#{BASE_URL}/#{path}#{params}" - body = kwargs[:body] - response = kwargs[:response] - response ||= key ? single_response(name) : response(name) - - stub = stub_request(method, url) - stub = stub.with(body:) if body - stub = stub.to_return(status: 200, body: response) if response - stub - end - - def new_contact - entity(:contacts).new(default_client).to_hash - end - - def default_client - clients(:puzzle) - end - - def client_with_key - default_client.invoicing_key = 1234 - default_client - end - - def single_response(name) - response(name.to_s.singularize) - end - - def response(name) - file_fixture("small_invoice/#{name}.json").read - rescue StandardError - nil - end - - def id(name) - data = JSON.parse( - file_fixture("small_invoice/#{name}.json").read - ) - - return data['items'].first['id'] if data.key? 'items' - - data['item']['id'] - rescue StandardError - nil - end - - def auth_body - JSON.generate( - { - access_token: '1234', - expires_in: 43_200, - token_type: 'Bearer' - } - ) - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb index f0fe47853..77e1e5c4a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -26,19 +26,6 @@ require 'rails/test_help' require 'mocha/minitest' require 'capybara/rails' - -require 'webmock/minitest' -WebMock.disable_net_connect!( - allow_localhost: true, # required for selenium - allow: [ - 'github.com', # required for webdrivers/geckodriver - /github-production-release-asset-\w+.s3.amazonaws.com/, # required for webdrivers/geckodriver - /github-releases.githubusercontent.com/, # required for webdrivers/geckodriver - /objects.githubusercontent.com/, # required for webdrivers/geckodriver - 'chromedriver.storage.googleapis.com' # required for webdrivers/chromedriver - ] -) - Settings.reload! Dir[Rails.root.join('test/support/**/*.rb')].each { |f| require f }