From 6635c4383c439cb451bcf03360f64c0b66ea40ea Mon Sep 17 00:00:00 2001 From: Vincent Pochet Date: Mon, 3 Oct 2022 14:35:30 +0200 Subject: [PATCH 1/6] feat(credit_notes): Expose credit notes in API --- .rubocop.yml | 4 + .../api/v1/credit_notes_controller.rb | 41 +++++++ app/models/customer.rb | 1 + app/serializers/v1/credit_note_serializer.rb | 34 ++++++ config/routes.rb | 1 + spec/requests/api/v1/credit_notes_spec.rb | 106 ++++++++++++++++++ 6 files changed, 187 insertions(+) create mode 100644 app/controllers/api/v1/credit_notes_controller.rb create mode 100644 app/serializers/v1/credit_note_serializer.rb create mode 100644 spec/requests/api/v1/credit_notes_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 88e8b544081..08a7e3035b8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -168,6 +168,10 @@ RSpec/MultipleMemoizedHelpers: RSpec/NestedGroups: Enabled: false +RSpec/FilePath: + Exclude: + - 'spec/requests/**/*' + Style/BlockDelimiters: Description: >- Avoid using {...} for multi-line blocks (multiline chaining is always ugly). diff --git a/app/controllers/api/v1/credit_notes_controller.rb b/app/controllers/api/v1/credit_notes_controller.rb new file mode 100644 index 00000000000..492b9711826 --- /dev/null +++ b/app/controllers/api/v1/credit_notes_controller.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Api + module V1 + class CreditNotesController < Api::BaseController + def show + credit_note = current_organization.credit_notes.find_by(id: params[:id]) + return not_found_error(resource: 'credit_note') unless credit_note + + render( + json: ::V1::CreditNoteSerializer.new( + credit_note, + root_name: 'credit_note', + includes: %i[fees], + ), + ) + end + + def index + credit_notes = current_organization.credit_notes + + if params[:external_customer_id] + credit_notes = credit_notes.joins(:customer).where(customers: { external_id: params[:external_customer_id] }) + end + + credit_notes = credit_notes.order(created_at: :desc) + .page(params[:page]) + .per(params[:per_page] || PER_PAGE) + + render( + json: ::CollectionSerializer.new( + credit_notes, + ::V1::CreditNoteSerializer, + collection_name: 'credit_notes', + meta: pagination_metadata(credit_notes), + ), + ) + end + end + end +end diff --git a/app/models/customer.rb b/app/models/customer.rb index d626de856b7..c3a6e67ce0d 100644 --- a/app/models/customer.rb +++ b/app/models/customer.rb @@ -13,6 +13,7 @@ class Customer < ApplicationRecord has_many :invoices has_many :applied_coupons has_many :coupons, through: :applied_coupons + has_many :credit_notes has_many :applied_add_ons has_many :add_ons, through: :applied_add_ons has_many :wallets diff --git a/app/serializers/v1/credit_note_serializer.rb b/app/serializers/v1/credit_note_serializer.rb new file mode 100644 index 00000000000..6ce21921bdf --- /dev/null +++ b/app/serializers/v1/credit_note_serializer.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module V1 + class CreditNoteSerializer < ModelSerializer + def serialize + payload = { + lago_id: model.id, + sequential_id: model.sequential_id, + number: model.number, + lago_invoice_id: model.invoice_id, + invoice_number: model.invoice.number, + status: model.status, + reason: model.reason, + amount_cents: model.amount_cents, + amount_currency: model.amount_currency, + remaining_amount_cents: model.remaining_amount_cents, + remaining_amount_currency: model.remaining_amount_currency, + created_at: model.created_at.iso8601, + updated_at: model.updated_at.iso8601, + file_url: nil, # TODO: Expose credit note document in API + } + + payload = payload.merge(fees) if include?(:fees) + + payload + end + + private + + def fees + ::CollectionSerializer.new(model.fees, ::V1::FeeSerializer, collection_name: 'fees').serialize + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 8bb8cb37e13..d385bcde8a9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,6 +24,7 @@ resources :add_ons, param: :code resources :billable_metrics, param: :code resources :coupons, param: :code + resources :credit_notes, only: %i[show index] resources :events, only: %i[create show] resources :applied_coupons, only: %i[create] resources :applied_add_ons, only: %i[create] diff --git a/spec/requests/api/v1/credit_notes_spec.rb b/spec/requests/api/v1/credit_notes_spec.rb new file mode 100644 index 00000000000..7f3f30fe1f1 --- /dev/null +++ b/spec/requests/api/v1/credit_notes_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Api::V1::CreditNotesController, type: :request do + let(:invoice) { create(:invoice) } + let(:organization) { invoice.organization } + let(:customer) { invoice.customer } + let(:credit_note) { create(:credit_note, invoice: invoice, customer: customer) } + let(:credit_note_items) { create_list(:credit_note_item, 2, credit_note: credit_note) } + + describe 'GET /credit_notes/:id' do + before { credit_note_items } + + it 'returns a credit note' do + get_with_token(organization, "/api/v1/credit_notes/#{credit_note.id}") + + aggregate_failures do + expect(response).to have_http_status(:success) + expect(json[:credit_note][:lago_id]).to eq(credit_note.id) + expect(json[:credit_note][:sequential_id]).to eq(credit_note.sequential_id) + expect(json[:credit_note][:number]).to eq(credit_note.number) + expect(json[:credit_note][:lago_invoice_id]).to eq(invoice.id) + expect(json[:credit_note][:invoice_number]).to eq(invoice.number) + expect(json[:credit_note][:status]).to eq(credit_note.status) + expect(json[:credit_note][:reason]).to eq(credit_note.reason) + expect(json[:credit_note][:amount_cents]).to eq(credit_note.amount_cents) + expect(json[:credit_note][:amount_currency]).to eq(credit_note.amount_currency) + expect(json[:credit_note][:remaining_amount_cents]).to eq(credit_note.remaining_amount_cents) + expect(json[:credit_note][:remaining_amount_currency]).to eq(credit_note.remaining_amount_currency) + expect(json[:credit_note][:created_at]).to eq(credit_note.created_at.iso8601) + expect(json[:credit_note][:updated_at]).to eq(credit_note.updated_at.iso8601) + + expect(json[:credit_note][:fees].count).to eq(2) + end + end + + context 'when credit note does not exists' do + it 'returns not found' do + get_with_token(organization, '/api/v1/credit_notes/foo') + + expect(response).to have_http_status(:not_found) + end + end + + context 'when credit note belongs to another organization' do + let(:wrong_credit_note) { create(:credit_note) } + + it 'returns not found' do + get_with_token(organization, "/api/v1/credit_notes/#{wrong_credit_note.id}") + end + end + end + + describe 'GET /credits_notes' do + let(:second_customer) { create(:customer, organization: organization) } + let(:second_invoice) { create(:invoice, customer: second_customer) } + let(:second_credit_note) { create(:credit_note, invoice: second_invoice, customer: second_invoice.customer) } + + before do + credit_note + second_credit_note + end + + it 'returns a list of credit_notes' do + get_with_token(organization, '/api/v1/credit_notes') + + aggregate_failures do + expect(response).to have_http_status(:success) + expect(json[:credit_notes].count).to eq(2) + expect(json[:credit_notes].first[:lago_id]).to eq(second_credit_note.id) + expect(json[:credit_notes].last[:lago_id]).to eq(credit_note.id) + end + end + + context 'with pagination' do + it 'returns the metadata' do + get_with_token(organization, '/api/v1/credit_notes?page=1&per_page=1') + + aggregate_failures do + expect(response).to have_http_status(:success) + expect(json[:credit_notes].count).to eq(1) + + expect(json[:meta][:current_page]).to eq(1) + expect(json[:meta][:next_page]).to eq(2) + expect(json[:meta][:prev_page]).to eq(nil) + expect(json[:meta][:total_pages]).to eq(2) + expect(json[:meta][:total_count]).to eq(2) + end + end + end + + context 'with external_customer_id filter' do + it 'returns credit notes of the customer' do + get_with_token(organization, "/api/v1/credit_notes?external_customer_id=#{customer.external_id}") + + aggregate_failures do + expect(response).to have_http_status(:success) + + expect(json[:credit_notes].count).to eq(1) + expect(json[:credit_notes].first[:lago_id]).to eq(credit_note.id) + end + end + end + end +end From 4d21b377e390f16ed9fce4b7b8de18c184389ed5 Mon Sep 17 00:00:00 2001 From: Vincent Pochet Date: Mon, 3 Oct 2022 16:16:26 +0200 Subject: [PATCH 2/6] Fix specs --- spec/factories/billable_metric_factory.rb | 2 +- spec/factories/credit_note_items.rb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/factories/billable_metric_factory.rb b/spec/factories/billable_metric_factory.rb index c9da7f3b900..d029d3f5458 100644 --- a/spec/factories/billable_metric_factory.rb +++ b/spec/factories/billable_metric_factory.rb @@ -5,7 +5,7 @@ organization name { 'Some metric' } description { 'some description' } - code { Faker::Name.first_name } + code { Faker::Alphanumeric.alphanumeric(number: 10) } aggregation_type { 'count_agg' } properties { {} } end diff --git a/spec/factories/credit_note_items.rb b/spec/factories/credit_note_items.rb index 68bc4781579..e655aa5bc91 100644 --- a/spec/factories/credit_note_items.rb +++ b/spec/factories/credit_note_items.rb @@ -4,5 +4,7 @@ factory :credit_note_item do credit_note fee + amount_cents { 100 } + amount_currency { 'EUR' } end end From fd41bbc5837ea7a8d68b21102c9b26e0f0640b5b Mon Sep 17 00:00:00 2001 From: Vincent Pochet Date: Mon, 3 Oct 2022 17:02:20 +0200 Subject: [PATCH 3/6] Item serializer --- .../api/v1/credit_notes_controller.rb | 2 +- .../v1/credit_note_item_serializer.rb | 23 +++++++++++++++++++ app/serializers/v1/credit_note_serializer.rb | 10 +++++--- spec/factories/plan_factory.rb | 2 +- spec/requests/api/v1/credit_notes_spec.rb | 14 ++++++++++- 5 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 app/serializers/v1/credit_note_item_serializer.rb diff --git a/app/controllers/api/v1/credit_notes_controller.rb b/app/controllers/api/v1/credit_notes_controller.rb index 492b9711826..dbac10bb886 100644 --- a/app/controllers/api/v1/credit_notes_controller.rb +++ b/app/controllers/api/v1/credit_notes_controller.rb @@ -11,7 +11,7 @@ def show json: ::V1::CreditNoteSerializer.new( credit_note, root_name: 'credit_note', - includes: %i[fees], + includes: %i[items], ), ) end diff --git a/app/serializers/v1/credit_note_item_serializer.rb b/app/serializers/v1/credit_note_item_serializer.rb new file mode 100644 index 00000000000..9d209a01119 --- /dev/null +++ b/app/serializers/v1/credit_note_item_serializer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module V1 + class CreditNoteItemSerializer < ModelSerializer + def serialize + { + lago_id: model.id, + lago_fee_id: fee.id, + amount_cents: model.amount_cents, + amount_currency: model.amount_currency, + fee_amount_cents: fee.amount_cents, + fee_amount_currency: fee.amount_currency, + fee_item: { + type: fee.fee_type, + code: fee.item_code, + name: fee.item_name, + }, + } + end + + delegate :fee, to: :model + end +end diff --git a/app/serializers/v1/credit_note_serializer.rb b/app/serializers/v1/credit_note_serializer.rb index 6ce21921bdf..c69a269e06d 100644 --- a/app/serializers/v1/credit_note_serializer.rb +++ b/app/serializers/v1/credit_note_serializer.rb @@ -20,15 +20,19 @@ def serialize file_url: nil, # TODO: Expose credit note document in API } - payload = payload.merge(fees) if include?(:fees) + payload = payload.merge(items) if include?(:items) payload end private - def fees - ::CollectionSerializer.new(model.fees, ::V1::FeeSerializer, collection_name: 'fees').serialize + def items + ::CollectionSerializer.new( + model.items.order(created_at: :asc), + ::V1::CreditNoteItemSerializer, + collection_name: 'items', + ).serialize end end end diff --git a/spec/factories/plan_factory.rb b/spec/factories/plan_factory.rb index 08e7d4982d7..e1657fa416e 100644 --- a/spec/factories/plan_factory.rb +++ b/spec/factories/plan_factory.rb @@ -4,7 +4,7 @@ factory :plan do organization name { Faker::TvShows::SiliconValley.app } - code { Faker::Name.first_name } + code { Faker::Alphanumeric.alphanumeric(number: 10) } interval { 'monthly' } pay_in_advance { false } amount_cents { 100 } diff --git a/spec/requests/api/v1/credit_notes_spec.rb b/spec/requests/api/v1/credit_notes_spec.rb index 7f3f30fe1f1..3617a645ca4 100644 --- a/spec/requests/api/v1/credit_notes_spec.rb +++ b/spec/requests/api/v1/credit_notes_spec.rb @@ -31,7 +31,19 @@ expect(json[:credit_note][:created_at]).to eq(credit_note.created_at.iso8601) expect(json[:credit_note][:updated_at]).to eq(credit_note.updated_at.iso8601) - expect(json[:credit_note][:fees].count).to eq(2) + expect(json[:credit_note][:items].count).to eq(2) + + json_item = json[:credit_note][:items].first + item = credit_note_items.first + expect(json_item[:lago_id]).to eq(item.id) + expect(json_item[:lago_fee_id]).to eq(item.fee.id) + expect(json_item[:amount_cents]).to eq(item.amount_cents) + expect(json_item[:amount_currency]).to eq(item.amount_currency) + expect(json_item[:fee_amount_cents]).to eq(item.fee.amount_cents) + expect(json_item[:fee_amount_currency]).to eq(item.fee.amount_currency) + expect(json_item[:fee_item][:type]).to eq(item.fee.fee_type) + expect(json_item[:fee_item][:code]).to eq(item.fee.item_code) + expect(json_item[:fee_item][:name]).to eq(item.fee.item_name) end end From e9e39efb09057a2007243181a9666c829484c019 Mon Sep 17 00:00:00 2001 From: Vincent Pochet Date: Tue, 4 Oct 2022 09:25:34 +0200 Subject: [PATCH 4/6] Use fee serializer --- .../v1/credit_note_item_serializer.rb | 17 ++++++++--------- app/serializers/v1/fee_serializer.rb | 1 + spec/requests/api/v1/credit_notes_spec.rb | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/serializers/v1/credit_note_item_serializer.rb b/app/serializers/v1/credit_note_item_serializer.rb index 9d209a01119..4d795c799d3 100644 --- a/app/serializers/v1/credit_note_item_serializer.rb +++ b/app/serializers/v1/credit_note_item_serializer.rb @@ -5,19 +5,18 @@ class CreditNoteItemSerializer < ModelSerializer def serialize { lago_id: model.id, - lago_fee_id: fee.id, amount_cents: model.amount_cents, amount_currency: model.amount_currency, - fee_amount_cents: fee.amount_cents, - fee_amount_currency: fee.amount_currency, - fee_item: { - type: fee.fee_type, - code: fee.item_code, - name: fee.item_name, - }, + fee: fee, } end - delegate :fee, to: :model + private + + def fee + ::V1::FeeSerializer.new( + model.fee, + ).serialize + end end end diff --git a/app/serializers/v1/fee_serializer.rb b/app/serializers/v1/fee_serializer.rb index ac0eb299098..b852d26bd8b 100644 --- a/app/serializers/v1/fee_serializer.rb +++ b/app/serializers/v1/fee_serializer.rb @@ -4,6 +4,7 @@ module V1 class FeeSerializer < ModelSerializer def serialize { + lago_id: model.id, item: { type: model.fee_type, code: model.item_code, diff --git a/spec/requests/api/v1/credit_notes_spec.rb b/spec/requests/api/v1/credit_notes_spec.rb index 3617a645ca4..577a64e3b5f 100644 --- a/spec/requests/api/v1/credit_notes_spec.rb +++ b/spec/requests/api/v1/credit_notes_spec.rb @@ -36,14 +36,14 @@ json_item = json[:credit_note][:items].first item = credit_note_items.first expect(json_item[:lago_id]).to eq(item.id) - expect(json_item[:lago_fee_id]).to eq(item.fee.id) expect(json_item[:amount_cents]).to eq(item.amount_cents) expect(json_item[:amount_currency]).to eq(item.amount_currency) - expect(json_item[:fee_amount_cents]).to eq(item.fee.amount_cents) - expect(json_item[:fee_amount_currency]).to eq(item.fee.amount_currency) - expect(json_item[:fee_item][:type]).to eq(item.fee.fee_type) - expect(json_item[:fee_item][:code]).to eq(item.fee.item_code) - expect(json_item[:fee_item][:name]).to eq(item.fee.item_name) + expect(json_item[:fee][:lago_id]).to eq(item.fee.id) + expect(json_item[:fee][:amount_cents]).to eq(item.fee.amount_cents) + expect(json_item[:fee][:amount_currency]).to eq(item.fee.amount_currency) + expect(json_item[:fee][:item][:type]).to eq(item.fee.fee_type) + expect(json_item[:fee][:item][:code]).to eq(item.fee.item_code) + expect(json_item[:fee][:item][:name]).to eq(item.fee.item_name) end end From 128acd5c9cac171d5ff39c8100726b5215883284 Mon Sep 17 00:00:00 2001 From: Vincent Pochet Date: Tue, 4 Oct 2022 15:29:43 +0200 Subject: [PATCH 5/6] Add request specs in controller folder --- .../api/v1/credit_notes_controller_spec.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/{requests/api/v1/credit_notes_spec.rb => controllers/api/v1/credit_notes_controller_spec.rb} (100%) diff --git a/spec/requests/api/v1/credit_notes_spec.rb b/spec/controllers/api/v1/credit_notes_controller_spec.rb similarity index 100% rename from spec/requests/api/v1/credit_notes_spec.rb rename to spec/controllers/api/v1/credit_notes_controller_spec.rb From 187d1c375dbdc99ab9225960e9ebdb6655fb1fb5 Mon Sep 17 00:00:00 2001 From: Vincent Pochet Date: Tue, 4 Oct 2022 15:32:27 +0200 Subject: [PATCH 6/6] No need for update in rubocop --- .rubocop.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 08a7e3035b8..88e8b544081 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -168,10 +168,6 @@ RSpec/MultipleMemoizedHelpers: RSpec/NestedGroups: Enabled: false -RSpec/FilePath: - Exclude: - - 'spec/requests/**/*' - Style/BlockDelimiters: Description: >- Avoid using {...} for multi-line blocks (multiline chaining is always ugly).