diff --git a/app/controllers/api/v1/customer_account_transaction_controller.rb b/app/controllers/api/v1/customer_account_transaction_controller.rb new file mode 100644 index 0000000000..d48e41f0a7 --- /dev/null +++ b/app/controllers/api/v1/customer_account_transaction_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Api + module V1 + class CustomerAccountTransactionController < Api::V1::BaseController + def create + authorize! :create, CustomerAccountTransaction + + # We only allow using the api customer credit payment method + default_params = { currency: CurrentConfig.get(:currency), payment_method_id: } + transaction = CustomerAccountTransaction.new( + default_params.merge(customer_account_transaction_params) + ) + + if transaction.save + render json: Api::V1::CustomerAccountTransactionSerializer.new(transaction), + status: :created + else + invalid_resource! transaction + end + end + + private + + def customer_account_transaction_params + params.require(:customer_account_transaction).permit(:customer_id, :amount, :description) + end + + def payment_method_id + Spree::PaymentMethod.internal.find_by( + name: Rails.application.config.api_payment_method[:name] + )&.id + end + end + end +end diff --git a/app/json_schemas/customer_account_transaction_schema.rb b/app/json_schemas/customer_account_transaction_schema.rb new file mode 100644 index 0000000000..4f5f1c5b76 --- /dev/null +++ b/app/json_schemas/customer_account_transaction_schema.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class CustomerAccountTransactionSchema < JsonApiSchema + def self.object_name + "customer_account_transaction" + end + + def self.attributes + { + id: { type: :integer, example: 1 }, + customer_id: { type: :integer, example: 10 }, + amount: { type: :decimal, example: 10.50 }, + currency: { type: :string, example: "AUD" }, + payment_method_id: { type: :integer, example: 100 }, + description: { type: :string, nullable: true, example: "Payment processed by POS" }, + balance: { type: :decimal, example: 10.50 }, + } + end + + def self.required_attributes + [:customer_id, :amount] + end + + def self.writable_attributes + attributes.except(:id, :balance, :payment_method_id, :currency) + end + + def self.relationships + [:customer, :payment_method] + end +end diff --git a/app/models/spree/ability.rb b/app/models/spree/ability.rb index 69aeabf2cf..458cee2b44 100644 --- a/app/models/spree/ability.rb +++ b/app/models/spree/ability.rb @@ -61,6 +61,7 @@ module Spree add_manage_line_items_abilities user end add_relationship_management_abilities user if can_manage_relationships? user + add_customer_payment_abilities user if can_manage_enterprises? user end # New users have no enterprises. @@ -457,5 +458,9 @@ module Spree user.enterprises.include?(enterprise_relationship.child) end end + + def add_customer_payment_abilities(_user) + can [:create], CustomerAccountTransaction + end end end diff --git a/app/serializers/api/v1/customer_account_transaction_serializer.rb b/app/serializers/api/v1/customer_account_transaction_serializer.rb new file mode 100644 index 0000000000..7f2031b82d --- /dev/null +++ b/app/serializers/api/v1/customer_account_transaction_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Api + module V1 + class CustomerAccountTransactionSerializer < Api::V1::BaseSerializer + attributes :id, :customer_id, :payment_method_id, :amount, :currency, :description, :balance + end + end +end diff --git a/config/routes/api.rb b/config/routes/api.rb index a877326a68..4df930deaf 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -92,6 +92,8 @@ Openfoodnetwork::Application.routes.draw do resources :enterprises do resources :customers, only: :index end + + resources :customer_account_transaction, only: [:create] end match '*path', to: redirect(path: "/api/v0/%{path}"), via: :all, diff --git a/spec/requests/api/v1/customer_account_transaction_spec.rb b/spec/requests/api/v1/customer_account_transaction_spec.rb new file mode 100644 index 0000000000..be7b5c0eb6 --- /dev/null +++ b/spec/requests/api/v1/customer_account_transaction_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require "swagger_helper" + +RSpec.describe "CustomerAccountTransactions", swagger_doc: "v1.yaml", feature: :api_v1 do + let!(:enterprise) { create(:enterprise) } + let(:payment_method) { + create( + :payment_method, + name: CustomerAccountTransaction::DEFAULT_PAYMENT_METHOD_NAME, + distributors: [enterprise] + ) + } + let(:customer) { create(:customer) } + + before do + login_as enterprise.owner + end + + path "/api/v1/customer_account_transaction" do + post "Create customer transaction" do + tags "Customer account transaction" + consumes "application/json" + produces "application/json" + + parameter name: :customer_account_transaction, in: :body, schema: { + type: :object, + properties: CustomerAccountTransactionSchema.writable_attributes, + required: CustomerAccountTransactionSchema.required_attributes + } + + response "201", "Customer transaction created" do + let(:customer_account_transaction) do + { + customer_id: customer.id.to_s, + amount: "10.25", + description: "Payment processed by POS" + } + end + schema '$ref': "#/components/schemas/customer_account_transaction" + + run_test! do + expect(json_response[:data][:attributes]).to include( + customer_id: customer.id, + payment_method_id: payment_method.id, + amount: "10.25", + currency: "AUD", + description: "Payment processed by POS", + balance: "10.25", + ) + + transaction = CustomerAccountTransaction.find(json_response[:data][:attributes][:id]) + expect(transaction).not_to be_nil + end + end + + response "422", "Unpermitted parameter" do + let(:customer_account_transaction) do + { + id: 101, + customer_id: customer.id.to_s, + amount: "10.25", + } + end + schema '$ref': "#/components/schemas/error_response" + + run_test! do + expect(json_response[:errors][0][:detail]).to eq( + "Parameters not allowed in this request: id" + ) + end + end + + response "422", "Unprocessable entity" do + let(:customer_account_transaction) { {} } + schema '$ref': "#/components/schemas/error_response" + + run_test! do + expect(json_response[:errors][0][:detail]).to eq( + "A required parameter is missing or empty: customer_account_transaction" + ) + expect(json_response[:meta]).to eq nil + end + end + + response "422", "Invalid resource" do + let(:customer_account_transaction) { { amount: "10.25" } } + schema '$ref': "#/components/schemas/error_response" + + run_test! do + expect(json_response[:errors][0][:detail]).to eq( + "Invalid resource. Please fix errors and try again." + ) + expect(json_response[:meta][:validation_errors]).to eq ["Customer must exist"] + end + end + + response "401", "Unauthorized" do + before { login_as nil } + + let(:customer_account_transaction) do + { + customer_id: customer.id.to_s, + amount: "10.25", + } + end + + run_test! + end + end + end +end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index 51bfb098fa..d4ab6660a9 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -27,7 +27,9 @@ RSpec.configure do |config| error_response: ErrorsSchema.schema, # only customer#show is with extra_fields: {name: :balance, required: true} customer: CustomerSchema.schema(require_all: true), - customers_collection: CustomerSchema.collection(require_all: true, extra_fields: :balance) + customers_collection: CustomerSchema.collection(require_all: true, + extra_fields: :balance), + customer_account_transaction: CustomerAccountTransactionSchema.schema(require_all: true) }, securitySchemes: { api_key_header: { diff --git a/swagger/v1.yaml b/swagger/v1.yaml index a9305be001..65410c4222 100644 --- a/swagger/v1.yaml +++ b/swagger/v1.yaml @@ -77,7 +77,7 @@ components: billing_address: type: object nullable: true - example: + example: shipping_address: type: object nullable: true @@ -190,7 +190,7 @@ components: billing_address: type: object nullable: true - example: + example: shipping_address: type: object nullable: true @@ -283,6 +283,92 @@ components: - data - meta - links + customer_account_transaction: + type: object + properties: + data: + type: object + properties: + id: + type: string + example: '1' + type: + type: string + example: customer_account_transaction + attributes: + type: object + properties: + id: + type: integer + example: 1 + customer_id: + type: integer + example: 10 + amount: + type: decimal + example: 10.5 + currency: + type: string + example: AUD + payment_method_id: + type: integer + example: 100 + description: + type: string + nullable: true + example: Payment processed by POS + balance: + type: decimal + example: 10.5 + required: + - id + - customer_id + - amount + - currency + - payment_method_id + - description + - balance + relationships: + type: object + properties: + customer: + type: object + properties: + data: + type: object + properties: + id: + type: string + type: + type: string + example: customer + links: + type: object + properties: + related: + type: string + payment_method: + type: object + properties: + data: + type: object + properties: + id: + type: string + type: + type: string + example: payment_method + links: + type: object + properties: + related: + type: string + meta: + type: object + links: + type: object + required: + - data securitySchemes: api_key_header: type: apiKey @@ -300,6 +386,46 @@ components: in: cookie description: Authenticates using the current user's session if logged in paths: + "/api/v1/customer_account_transaction": + post: + summary: Create customer transaction + tags: + - Customer account transaction + parameters: [] + responses: + '201': + description: Customer transaction created + content: + application/json: + schema: + "$ref": "#/components/schemas/customer_account_transaction" + '422': + description: Invalid resource + content: + application/json: + schema: + "$ref": "#/components/schemas/error_response" + '401': + description: Unauthorized + requestBody: + content: + application/json: + schema: + type: object + properties: + customer_id: + type: integer + example: 10 + amount: + type: decimal + example: 10.5 + description: + type: string + nullable: true + example: Payment processed by POS + required: + - customer_id + - amount "/api/v1/customers": get: summary: List customers @@ -375,7 +501,7 @@ paths: billing_address: type: object nullable: true - example: + example: shipping_address: type: object nullable: true @@ -468,7 +594,7 @@ paths: billing_address: type: object nullable: true - example: + example: shipping_address: type: object nullable: true @@ -598,7 +724,7 @@ paths: billing_address: type: object nullable: true - example: + example: shipping_address: type: object nullable: true