diff --git a/app/config/permissions/definition.yml b/app/config/permissions/definition.yml index d6739b33f786..0b03e69fd52f 100644 --- a/app/config/permissions/definition.yml +++ b/app/config/permissions/definition.yml @@ -86,3 +86,4 @@ organization: delete: payment_requests: view: true + create: diff --git a/app/config/permissions/role-finance.yml b/app/config/permissions/role-finance.yml index 602e10986e47..573d2d20b904 100644 --- a/app/config/permissions/role-finance.yml +++ b/app/config/permissions/role-finance.yml @@ -62,3 +62,4 @@ organization: delete: false payment_requests: view: true + create: true diff --git a/app/config/permissions/role-manager.yml b/app/config/permissions/role-manager.yml index 2509396771ae..bdd90538173f 100644 --- a/app/config/permissions/role-manager.yml +++ b/app/config/permissions/role-manager.yml @@ -62,3 +62,4 @@ organization: delete: false payment_requests: view: true + create: true diff --git a/app/controllers/api/v1/payment_requests_controller.rb b/app/controllers/api/v1/payment_requests_controller.rb index bc8684b21305..7e57f34ef4d0 100644 --- a/app/controllers/api/v1/payment_requests_controller.rb +++ b/app/controllers/api/v1/payment_requests_controller.rb @@ -3,6 +3,19 @@ module Api module V1 class PaymentRequestsController < Api::BaseController + def create + result = PaymentRequests::CreateService.call( + organization: current_organization, + params: create_params.to_h.deep_symbolize_keys + ) + + if result.success? + render(json: ::V1::PaymentRequestSerializer.new(result.payment_request, root_name: "payment_request")) + else + render_error_response(result) + end + end + def index result = PaymentRequestsQuery.call( organization: current_organization, @@ -30,6 +43,14 @@ def index private + def create_params + params.require(:payment_request).permit( + :email, + :external_customer_id, + :lago_invoice_ids + ) + end + def index_filters params.permit(:external_customer_id) end diff --git a/app/graphql/mutations/payment_requests/create.rb b/app/graphql/mutations/payment_requests/create.rb new file mode 100644 index 000000000000..abbf94049984 --- /dev/null +++ b/app/graphql/mutations/payment_requests/create.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Mutations + module PaymentRequests + class Create < BaseMutation + include AuthenticableApiUser + include RequiredOrganization + + REQUIRED_PERMISSION = "payment_requests:create" + + graphql_name "CreatePaymentRequest" + description "Creates a payment request" + + input_object_class Types::PaymentRequests::CreateInput + type Types::PaymentRequests::Object + + def resolve(**args) + result = ::PaymentRequests::CreateService.call(organization: current_organization, params: args) + result.success? ? result.payment_request : result_error(result) + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 4b089f21bd3b..46a8ac1f9860 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -106,6 +106,8 @@ class MutationType < Types::BaseObject field :revoke_membership, mutation: Mutations::Memberships::Revoke field :update_membership, mutation: Mutations::Memberships::Update + field :create_payment_request, mutation: Mutations::PaymentRequests::Create + field :create_password_reset, mutation: Mutations::PasswordResets::Create field :reset_password, mutation: Mutations::PasswordResets::Reset diff --git a/app/graphql/types/payment_requests/create_input.rb b/app/graphql/types/payment_requests/create_input.rb new file mode 100644 index 000000000000..fbbd473d9158 --- /dev/null +++ b/app/graphql/types/payment_requests/create_input.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module PaymentRequests + class CreateInput < Types::BaseInputObject + graphql_name "PaymentRequestCreateInput" + + argument :external_customer_id, String, required: true + + argument :email, String, required: false + argument :lago_invoice_ids, [String], required: false + end + end +end diff --git a/app/services/payment_requests/create_service.rb b/app/services/payment_requests/create_service.rb new file mode 100644 index 000000000000..ce70de77e4ff --- /dev/null +++ b/app/services/payment_requests/create_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module PaymentRequests + class CreateService < BaseService + def initialize(organization:, params:) + @organization = organization + @params = params + + super + end + + def call + return result.not_found_failure!(resource: "customer") unless customer + + ActiveRecord::Base.transaction do + # TODO: Create payable group for the overdue invoices + + # TODO: Create payment request for the payable group + + # TODO: Send payment_request.created webhook + + # TODO: When payment provider is set: Create payment intent for the overdue invoices + # TODO: When payment provider is not set: Send email to the customer + + # result.payment_request = payment_request + end + + result + end + + private + + attr_reader :organization, :params + + def customer + @customer ||= organization.customers.find_by(external_id: params[:external_customer_id]) + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 54bc1f390dfb..77d602e64b52 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -61,7 +61,7 @@ put :refresh, on: :member put :finalize, on: :member end - resources :payment_requests, only: %i[index] + resources :payment_requests, only: %i[create index] resources :plans, param: :code resources :taxes, param: :code resources :wallet_transactions, only: :create diff --git a/schema.graphql b/schema.graphql index 4dc359a1381b..82c96aac5097 100644 --- a/schema.graphql +++ b/schema.graphql @@ -5304,6 +5304,7 @@ type Permissions { organizationTaxesView: Boolean! organizationUpdate: Boolean! organizationView: Boolean! + paymentRequestsCreate: Boolean! paymentRequestsView: Boolean! plansCreate: Boolean! plansDelete: Boolean! diff --git a/schema.json b/schema.json index 67f6eebbedb4..69860e9397b5 100644 --- a/schema.json +++ b/schema.json @@ -26383,6 +26383,24 @@ ] }, + { + "name": "paymentRequestsCreate", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, { "name": "paymentRequestsView", "description": null, diff --git a/spec/graphql/mutations/payment_requests/create_spec.rb b/spec/graphql/mutations/payment_requests/create_spec.rb new file mode 100644 index 000000000000..9edac7801cea --- /dev/null +++ b/spec/graphql/mutations/payment_requests/create_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Mutations::PaymentRequests::Create, type: :graphql do + let(:required_permission) { "payment_requests:create" } + let(:membership) { create(:membership) } + let(:organization) { membership.organization } + let(:customer) { create(:customer, organization:) } + let(:invoice1) { create(:invoice, organization:) } + let(:invoice2) { create(:invoice, organization:) } + + let(:input) do + { + email: "john.doe@example.com", + externalCustomerId: customer.external_id, + lagoInvoiceIds: [invoice1.id, invoice2.id] + } + end + + let(:mutation) do + <<-GQL + mutation($input: PaymentRequestCreateInput!) { + createPaymentRequest(input: $input) { + id + email + customer { id } + invoices { id } + } + } + GQL + end + + it "creates a payment request" do + result = execute_graphql( + current_user: membership.user, + current_organization: membership.organization, + permissions: required_permission, + query: mutation, + variables: {input:} + ) + + expect(result["data"]).to include( + "createPaymentRequest" => nil + ) + end +end diff --git a/spec/requests/api/v1/payment_requests_controller_spec.rb b/spec/requests/api/v1/payment_requests_controller_spec.rb index 5f41059e81e7..7a48a6057ff5 100644 --- a/spec/requests/api/v1/payment_requests_controller_spec.rb +++ b/spec/requests/api/v1/payment_requests_controller_spec.rb @@ -5,6 +5,47 @@ RSpec.describe Api::V1::PaymentRequestsController, type: :request do let(:organization) { create(:organization) } + describe "create" do + let(:customer) { create(:customer, organization:) } + let(:params) do + { + email: customer.email, + external_customer_id: customer.external_id + } + end + + context "when customer is not found" do + let(:params) do + {external_customer_id: "unknown"} + end + + it "returns a not found error" do + post_with_token(organization, "/api/v1/payment_requests", {payment_request: params}) + expect(response).to have_http_status(:not_found) + end + end + + it "delegates to PaymentRequests::CreateService", :aggregate_failures do + payment_request = create(:payment_request) + allow(PaymentRequests::CreateService).to receive(:call).and_return( + BaseService::Result.new.tap { |r| r.payment_request = payment_request } + ) + + post_with_token(organization, "/api/v1/payment_requests", {payment_request: params}) + + expect(PaymentRequests::CreateService).to have_received(:call).with( + organization:, + params: { + email: customer.email, + external_customer_id: customer.external_id + } + ) + + expect(response).to have_http_status(:success) + expect(json[:payment_request][:lago_id]).to eq(payment_request.id) + end + end + describe "index" do it "returns organization's payment requests", :aggregate_failures do first_customer = create(:customer, organization:)