diff --git a/app/controllers/api/v1/user/financial_transactions_controller.rb b/app/controllers/api/v1/user/financial_transactions_controller.rb index c042db32..947bc4aa 100644 --- a/app/controllers/api/v1/user/financial_transactions_controller.rb +++ b/app/controllers/api/v1/user/financial_transactions_controller.rb @@ -3,6 +3,8 @@ class Api::V1::User::FinancialTransactionsController < Api::V1::BaseController before_action ->{ doorkeeper_authorize! 'finance:user' } before_action :require_ordergroup + before_action :require_minimum_balance, only: [:create] + before_action -> { require_config_enabled :use_self_service }, only: [:create] def index render_collection search_scope @@ -12,10 +14,20 @@ class Api::V1::User::FinancialTransactionsController < Api::V1::BaseController render json: scope.find(params.require(:id)) end + def create + transaction_type = FinancialTransactionType.find(create_params[:financial_transaction_type_id]) + ft = current_ordergroup.add_financial_transaction!(create_params[:amount], create_params[:note], current_user, transaction_type) + render json: ft + end + private def scope current_ordergroup.financial_transactions.includes(:user, :financial_transaction_type) end + def create_params + params.require(:financial_transaction).permit(:amount, :financial_transaction_type_id, :note) + end + end diff --git a/app/models/ordergroup.rb b/app/models/ordergroup.rb index e65bd1a0..da16db07 100644 --- a/app/models/ordergroup.rb +++ b/app/models/ordergroup.rb @@ -90,6 +90,7 @@ class Ordergroup < Group if t.amount < 0 && self.account_balance < 0 && self.account_balance - t.amount >= 0 Resque.enqueue(UserNotifier, FoodsoftConfig.scope, 'negative_balance', self.id, t.id) end + t end end diff --git a/config/routes.rb b/config/routes.rb index 676e8c36..07d32a4f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -265,7 +265,7 @@ Rails.application.routes.draw do namespace :user do root to: 'users#show' - resources :financial_transactions, only: [:index, :show] + resources :financial_transactions, only: [:index, :show, :create] resources :group_order_articles end diff --git a/doc/swagger.v1.yml b/doc/swagger.v1.yml index 1c5c4cc2..ce9cdb91 100644 --- a/doc/swagger.v1.yml +++ b/doc/swagger.v1.yml @@ -89,6 +89,42 @@ paths: $ref: '#/definitions/Error403' security: - foodsoft_auth: ['finance:user'] + post: + summary: create new financial transaction + tags: + - 1. User + - 6. FinancialTransaction + parameters: + - in: body + name: body + description: financial transaction to create + required: true + schema: + $ref: '#/definitions/FinancialTransactionForCreate' + responses: + 200: + description: success + schema: + type: object + properties: + financial_transaction: + $ref: '#/definitions/FinancialTransaction' + 401: + description: not logged-in + schema: + $ref: '#/definitions/Error401' + 403: + description: user has no ordergroup, is below minimum balance, or missing scope + schema: + $ref: '#/definitions/Error403' + 404: + description: financial transaction type not found + schema: + $ref: '#/definitions/Error404' + 422: + description: invalid parameter value + schema: + $ref: '#/definitions/Error422' /user/financial_transactions/{id}: parameters: - $ref: '#/parameters/idInUrl' @@ -719,34 +755,40 @@ definitions: description: language code required: ['id', 'name', 'email'] - FinancialTransaction: + FinancialTransactionForCreate: type: object properties: - id: - type: integer - user_id: - type: ['integer', 'null'] - description: id of user who entered the transaction (may be null for deleted users or 0 for a system user) - user_name: - type: ['string', 'null'] - description: name of user who entered the transaction (may be null or empty string for deleted users or system users) amount: type: number description: amount credited (negative for a debit transaction) financial_transaction_type_id: type: integer description: id of the type of the transaction - financial_transaction_type_name: - type: string - description: name of the type of the transaction note: type: string description: note entered with the transaction - created_at: - type: string - format: date-time - description: when the transaction was entered - required: ['id', 'user_id', 'user_name', 'amount', 'financial_transaction_type_id', 'financial_transaction_type_name', 'note', 'created_at'] + required: ['amount', 'financial_transaction_type_id', 'note'] + FinancialTransaction: + allOf: + - $ref: '#/definitions/FinancialTransactionForCreate' + - type: object + properties: + id: + type: integer + user_id: + type: ['integer', 'null'] + description: id of user who entered the transaction (may be null for deleted users or 0 for a system user) + user_name: + type: ['string', 'null'] + description: name of user who entered the transaction (may be null or empty string for deleted users or system users) + financial_transaction_type_name: + type: string + description: name of the type of the transaction + created_at: + type: string + format: date-time + description: when the transaction was entered + required: ['id', 'user_id', 'user_name', 'financial_transaction_type_name', 'created_at'] FinancialTransactionClass: type: object diff --git a/spec/api/v1/swagger_spec.rb b/spec/api/v1/swagger_spec.rb index 15b8311b..382f609d 100644 --- a/spec/api/v1/swagger_spec.rb +++ b/spec/api/v1/swagger_spec.rb @@ -48,7 +48,38 @@ describe 'API v1', type: :apivore, order: :defined do it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 404, api_auth({'id' => other_ft_1.id})) } it { is_expected.to validate(:get, '/user/financial_transactions/{id}', 404, api_auth({'id' => FinancialTransaction.last.id + 1})) } + let(:create_params) { {'_data' => {financial_transaction: {amount: 1, financial_transaction_type_id: ft_1.financial_transaction_type.id, note: 'note'}}} } + + + context 'without using self service' do + it { is_expected.to validate(:post, '/user/financial_transactions', 403, api_auth(create_params)) } + end + + context 'with using self service' do + before { FoodsoftConfig[:use_self_service] = true } + + it { is_expected.to validate(:post, '/user/financial_transactions', 200, api_auth(create_params)) } + + context 'with invalid financial transaction type' do + let(:create_params) { {'_data' => {financial_transaction: {amount: 1, financial_transaction_type_id: -1, note: 'note'}}} } + + it { is_expected.to validate(:post, '/user/financial_transactions', 404, api_auth(create_params)) } + end + + context 'without note' do + let(:create_params) { {'_data' => {financial_transaction: {amount: 1, financial_transaction_type_id: ft_1.financial_transaction_type.id}}} } + + it { is_expected.to validate(:post, '/user/financial_transactions', 422, api_auth(create_params)) } + end + + context 'without enough balance' do + before { FoodsoftConfig[:minimum_balance] = 1000 } + it { is_expected.to validate(:post, '/user/financial_transactions', 403, api_auth(create_params)) } + end + end + it_handles_invalid_token_and_scope(:get, '/user/financial_transactions') + it_handles_invalid_token_and_scope(:post, '/user/financial_transactions', ->{ api_auth(create_params) }) it_handles_invalid_token_and_scope(:get, '/user/financial_transactions/{id}', ->{ api_auth('id' => ft_2.id) }) end end diff --git a/spec/api/v1/user/financial_transactions_spec.rb b/spec/api/v1/user/financial_transactions_spec.rb new file mode 100644 index 00000000..d7f0c6b3 --- /dev/null +++ b/spec/api/v1/user/financial_transactions_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +# Most routes are tested in the swagger_spec, this tests endpoints that change data. +describe Api::V1::User::FinancialTransactionsController, type: :controller do + include ApiOAuth + let(:user) { create(:user, :ordergroup) } + let(:api_scopes) { ['finance:user'] } + + let(:ftc1) { create :financial_transaction_class } + let(:ftc2) { create :financial_transaction_class } + let(:ftt1) { create :financial_transaction_type, financial_transaction_class: ftc1 } + let(:ftt2) { create :financial_transaction_type, financial_transaction_class: ftc2 } + let(:ftt3) { create :financial_transaction_type, financial_transaction_class: ftc2 } + + let(:amount) { rand(-100..100) } + let(:note) { Faker::Lorem.sentence } + + let(:json_ft) { json_response['financial_transaction'] } + + shared_examples "financial_transactions endpoint success" do + before { request } + + it "returns status 200" do + expect(response.status).to eq 200 + end + end + + shared_examples "financial_transactions create/update success" do + include_examples "financial_transactions endpoint success" + + it "returns the financial_transaction" do + expect(json_ft['id']).to be_present + expect(json_ft['financial_transaction_type_id']).to eq ftt1.id + expect(json_ft['financial_transaction_type_name']).to eq ftt1.name + expect(json_ft['amount']).to eq amount + expect(json_ft['note']).to eq note + expect(json_ft['user_id']).to eq user.id + end + + it "updates the financial_transaction" do + resulting_ft = FinancialTransaction.where(id: json_ft['id']).first + expect(resulting_ft).to be_present + expect(resulting_ft.financial_transaction_type).to eq ftt1 + expect(resulting_ft.amount).to eq amount + expect(resulting_ft.note).to eq note + expect(resulting_ft.user).to eq user + end + end + + + shared_examples "financial_transactions endpoint failure" do |status| + it "returns status #{status}" do + request + expect(response.status).to eq status + end + + it "does not change the ordergroup" do + expect { request }.to_not change { + user.ordergroup.attributes + } + end + + it "does not change the financial_transactions of ordergroup" do + expect { request }.to_not change { + user.ordergroup.financial_transactions.count + } + end + end + + + describe "POST :create" do + let(:ft_params) { { amount: amount, financial_transaction_type_id: ftt1.id, note: note } } + let(:request) { post :create, params: { financial_transaction: ft_params, foodcoop: 'f' } } + + context 'without using self service' do + include_examples "financial_transactions endpoint failure", 403 + end + + context 'with using self service' do + before { FoodsoftConfig[:use_self_service] = true } + + context "with no existing financial transaction" do + include_examples "financial_transactions create/update success" + end + + context "with existing financial transaction" do + before { user.ordergroup.add_financial_transaction! 5000, 'for ordering', user, ftt3 } + include_examples "financial_transactions create/update success" + end + + context "with invalid financial transaction type" do + let(:ft_params) { { amount: amount, financial_transaction_type_id: -1, note: note } } + include_examples "financial_transactions endpoint failure", 404 + end + + context "without note" do + let(:ft_params) { { amount: amount, financial_transaction_type_id: ftt1.id } } + include_examples "financial_transactions endpoint failure", 422 + end + + context 'without enough balance' do + before { FoodsoftConfig[:minimum_balance] = 1000 } + include_examples "financial_transactions endpoint failure", 403 + end + end + end +end