From faeb2796d45f6ed149dc086df6d074d371e183cb Mon Sep 17 00:00:00 2001 From: Ivan Novosad Date: Fri, 21 Feb 2025 10:52:35 -0800 Subject: [PATCH] feat(payment-receipts): Add model methods and serializers --- app/models/organization.rb | 4 +- app/models/payment_receipt.rb | 21 ++++ .../v1/payment_receipt_serializer.rb | 20 ++++ app/serializers/v1/payment_serializer.rb | 13 ++- schema.graphql | 7 ++ schema.json | 18 ++++ spec/models/invoice_spec.rb | 1 + spec/models/organization_spec.rb | 2 +- spec/models/payment_receipt_spec.rb | 40 ++++++++ spec/models/payment_spec.rb | 1 + .../v1/payment_receipt_serializer_spec.rb | 27 ++++++ .../serializers/v1/payment_serializer_spec.rb | 97 +++++++++++++------ 12 files changed, 219 insertions(+), 32 deletions(-) create mode 100644 app/serializers/v1/payment_receipt_serializer.rb create mode 100644 spec/serializers/v1/payment_receipt_serializer_spec.rb diff --git a/app/models/organization.rb b/app/models/organization.rb index 140250a0113..0e4b805d67f 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -7,7 +7,8 @@ class Organization < ApplicationRecord EMAIL_SETTINGS = [ "invoice.finalized", - "credit_note.created" + "credit_note.created", + "payment_receipt.created" ].freeze has_many :api_keys @@ -78,6 +79,7 @@ class Organization < ApplicationRecord remove_branding_watermark manual_payments from_email + issue_receipts preview ].freeze PREMIUM_INTEGRATIONS = INTEGRATIONS - %w[anrok] diff --git a/app/models/payment_receipt.rb b/app/models/payment_receipt.rb index b12b86d5674..c572cddcb69 100644 --- a/app/models/payment_receipt.rb +++ b/app/models/payment_receipt.rb @@ -2,6 +2,27 @@ class PaymentReceipt < ApplicationRecord belongs_to :payment + + scope :for_organization, lambda { |organization| + payables_join = ActiveRecord::Base.sanitize_sql_array([ + <<~SQL, + INNER JOIN payments + ON payments.id = payment_receipts.payment_id + LEFT JOIN invoices + ON invoices.id = payments.payable_id + AND payments.payable_type = 'Invoice' + AND invoices.organization_id = :org_id + AND invoices.status IN (:visible_statuses) + LEFT JOIN payment_requests + ON payment_requests.id = payments.payable_id + AND payments.payable_type = 'PaymentRequest' + AND payment_requests.organization_id = :org_id + SQL + {org_id: organization.id, visible_statuses: Invoice::VISIBLE_STATUS.values} + ]) + joins(payables_join) + .where("invoices.id IS NOT NULL OR payment_requests.id IS NOT NULL") + } end # == Schema Information diff --git a/app/serializers/v1/payment_receipt_serializer.rb b/app/serializers/v1/payment_receipt_serializer.rb new file mode 100644 index 00000000000..fe0151eb1c2 --- /dev/null +++ b/app/serializers/v1/payment_receipt_serializer.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module V1 + class PaymentReceiptSerializer < ModelSerializer + def serialize + { + lago_id: model.id, + number: model.reload.number, + payment: payment, + created_at: model.created_at.iso8601 + } + end + + private + + def payment + ::V1::PaymentSerializer.new(model.payment).serialize + end + end +end diff --git a/app/serializers/v1/payment_serializer.rb b/app/serializers/v1/payment_serializer.rb index 94f1ce85b49..ed1eae153ca 100644 --- a/app/serializers/v1/payment_serializer.rb +++ b/app/serializers/v1/payment_serializer.rb @@ -3,7 +3,7 @@ module V1 class PaymentSerializer < ModelSerializer def serialize - { + payload = { lago_id: model.id, invoice_ids: invoice_id, amount_cents: model.amount_cents, @@ -14,10 +14,21 @@ def serialize external_payment_id: model.provider_payment_id, created_at: model.created_at.iso8601 } + + payload.merge!(payment_receipt) if include?(:payment_receipt) + payload end private + def payment_receipt + { + payment_receipt: model.payment_receipt ? + ::V1::PaymentReceiptSerializer.new(model.payment_receipt).serialize : + {} + } + end + def invoice_id model.payable.is_a?(Invoice) ? [model.payable.id] : model.payable.invoice_ids end diff --git a/schema.graphql b/schema.graphql index 30270697c83..2a683f943bc 100644 --- a/schema.graphql +++ b/schema.graphql @@ -3999,6 +3999,11 @@ enum EmailSettingsEnum { invoice.finalized """ invoice_finalized + + """ + payment_receipt.created + """ + payment_receipt_created } enum ErrorCodesEnum { @@ -4464,6 +4469,7 @@ enum IntegrationTypeEnum { auto_dunning from_email hubspot + issue_receipts manual_payments netsuite okta @@ -6515,6 +6521,7 @@ enum PremiumIntegrationTypeEnum { auto_dunning from_email hubspot + issue_receipts manual_payments netsuite okta diff --git a/schema.json b/schema.json index a847ccddc6b..a23397d6cb4 100644 --- a/schema.json +++ b/schema.json @@ -17173,6 +17173,12 @@ "description": "credit_note.created", "isDeprecated": false, "deprecationReason": null + }, + { + "name": "payment_receipt_created", + "description": "payment_receipt.created", + "isDeprecated": false, + "deprecationReason": null } ] }, @@ -20698,6 +20704,12 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "issue_receipts", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "preview", "description": null, @@ -31196,6 +31208,12 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "issue_receipts", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "preview", "description": null, diff --git a/spec/models/invoice_spec.rb b/spec/models/invoice_spec.rb index ea31d6bc5ce..2fd225bfc0d 100644 --- a/spec/models/invoice_spec.rb +++ b/spec/models/invoice_spec.rb @@ -17,6 +17,7 @@ it { is_expected.to have_many(:applied_payment_requests).class_name("PaymentRequest::AppliedInvoice") } it { is_expected.to have_many(:payment_requests).through(:applied_payment_requests) } it { is_expected.to have_many(:payments) } + it { is_expected.to have_many(:payment_receipts).through(:payments) } it { is_expected.to have_many(:applied_usage_thresholds) } it { is_expected.to have_many(:usage_thresholds).through(:applied_usage_thresholds) } diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb index c0549cfed8e..9d43f17ddea 100644 --- a/spec/models/organization_spec.rb +++ b/spec/models/organization_spec.rb @@ -124,7 +124,7 @@ end it "is valid with email_settings" do - organization.email_settings = ["invoice.finalized", "credit_note.created"] + organization.email_settings = ["invoice.finalized", "credit_note.created", "payment_receipt.created"] expect(organization).to be_valid end diff --git a/spec/models/payment_receipt_spec.rb b/spec/models/payment_receipt_spec.rb index dc3a41d00db..ecd7acc4a5f 100644 --- a/spec/models/payment_receipt_spec.rb +++ b/spec/models/payment_receipt_spec.rb @@ -6,4 +6,44 @@ subject(:payment_receipt) { build(:payment_receipt) } it { is_expected.to belong_to(:payment) } + + describe ".for_organization" do + subject(:result) { described_class.for_organization(organization) } + + let(:organization) { create(:organization) } + let(:visible_invoice) { create(:invoice, organization:, status: Invoice::VISIBLE_STATUS[:finalized]) } + let(:invisible_invoice) { create(:invoice, organization:, status: Invoice::INVISIBLE_STATUS[:generating]) } + let(:payment_request) { create(:payment_request, organization:) } + let(:other_org_payment_request) { create(:payment_request) } + + let(:visible_invoice_payment) { create(:payment, payable: visible_invoice) } + let!(:visible_invoice_payment_receipt) { create(:payment_receipt, payment: visible_invoice_payment) } + + let(:invisible_invoice_payment) { create(:payment, payable: invisible_invoice) } + let!(:invisible_invoice_payment_receipt) { create(:payment_receipt, payment: invisible_invoice_payment) } + + let(:payment_request_payment) { create(:payment, payable: payment_request) } + let!(:payment_request_payment_receipt) { create(:payment_receipt, payment: payment_request_payment) } + + let(:other_org_invoice_payment) { create(:payment) } + let!(:other_org_invoice_payment_receipt) do + create(:payment_receipt, payment: other_org_invoice_payment) + end + + let(:other_org_payment_request_payment) { create(:payment, payable: other_org_payment_request) } + + let(:other_org_payment_request_payment_receipt) do + create(:payment_receipt, payment: other_org_payment_request_payment) + end + + it "returns payments and payment requests for the organization's visible invoices" do + payments = subject + + expect(payments).to include(visible_invoice_payment_receipt) + expect(payments).to include(payment_request_payment_receipt) + expect(payments).not_to include(invisible_invoice_payment_receipt) + expect(payments).not_to include(other_org_invoice_payment_receipt) + expect(payments).not_to include(other_org_payment_request_payment_receipt) + end + end end diff --git a/spec/models/payment_spec.rb b/spec/models/payment_spec.rb index 3138e4238c9..5d7727bfb86 100644 --- a/spec/models/payment_spec.rb +++ b/spec/models/payment_spec.rb @@ -15,6 +15,7 @@ it_behaves_like "paper_trail traceable" it { is_expected.to have_many(:integration_resources) } + it { is_expected.to have_one(:payment_receipt) } it { is_expected.to belong_to(:payable) } it { is_expected.to delegate_method(:customer).to(:payable) } it { is_expected.to validate_presence_of(:payment_type) } diff --git a/spec/serializers/v1/payment_receipt_serializer_spec.rb b/spec/serializers/v1/payment_receipt_serializer_spec.rb new file mode 100644 index 00000000000..d636416ce10 --- /dev/null +++ b/spec/serializers/v1/payment_receipt_serializer_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe ::V1::PaymentReceiptSerializer do + subject(:serializer) do + described_class.new(payment_receipt, root_name: "payment_receipt") + end + + let(:payment_receipt) { create(:payment_receipt) } + + it "serializes the object" do + result = JSON.parse(serializer.to_json) + + expect(result["payment_receipt"]).to include( + "lago_id" => payment_receipt.id, + "number" => payment_receipt.number, + "created_at" => payment_receipt.created_at.iso8601 + ) + + expect(result["payment_receipt"]["payment"]).to include( + "lago_id" => payment_receipt.payment.id, + "amount_cents" => payment_receipt.payment.amount_cents, + "amount_currency" => payment_receipt.payment.amount_currency + ) + end +end diff --git a/spec/serializers/v1/payment_serializer_spec.rb b/spec/serializers/v1/payment_serializer_spec.rb index 3d699e76308..2e4c17053ae 100644 --- a/spec/serializers/v1/payment_serializer_spec.rb +++ b/spec/serializers/v1/payment_serializer_spec.rb @@ -4,26 +4,46 @@ RSpec.describe ::V1::PaymentSerializer do subject(:serializer) do - described_class.new(payment, root_name: "payment") + described_class.new(payment, root_name: "payment", includes:) end context "when payable is an invoice" do let(:payment) { create(:payment) } - it "serializes the object" do - result = JSON.parse(serializer.to_json) - - expect(result["payment"]).to include( - "lago_id" => payment.id, - "invoice_ids" => [payment.payable.id], - "amount_cents" => payment.amount_cents, - "amount_currency" => payment.amount_currency, - "payment_status" => payment.payable_payment_status, - "type" => payment.payment_type, - "reference" => payment.reference, - "external_payment_id" => payment.provider_payment_id, - "created_at" => payment.created_at.iso8601 - ) + context "when includes is empty" do + let(:includes) { [] } + + it "serializes the object" do + result = JSON.parse(serializer.to_json) + + expect(result["payment"]).to include( + "lago_id" => payment.id, + "invoice_ids" => [payment.payable.id], + "amount_cents" => payment.amount_cents, + "amount_currency" => payment.amount_currency, + "payment_status" => payment.payable_payment_status, + "type" => payment.payment_type, + "reference" => payment.reference, + "external_payment_id" => payment.provider_payment_id, + "created_at" => payment.created_at.iso8601 + ) + end + end + + context "when includes payment_receipt is set" do + let(:payment_receipt) { create(:payment_receipt) } + let(:payment) { payment_receipt.payment } + let(:includes) { %i[payment_receipt] } + + it "includes the payment receipt" do + result = JSON.parse(serializer.to_json) + + expect(result["payment"]["payment_receipt"]).to include( + "lago_id" => payment_receipt.id, + "number" => payment_receipt.number, + "created_at" => payment_receipt.created_at.iso8601 + ) + end end end @@ -35,20 +55,39 @@ create(:payment_request_applied_invoice, payment_request:) end - it "serializes the object" do - result = JSON.parse(serializer.to_json) - - expect(result["payment"]).to include( - "lago_id" => payment.id, - "invoice_ids" => payment_request.invoice_ids, - "amount_cents" => payment.amount_cents, - "amount_currency" => payment.amount_currency, - "payment_status" => payment.payable_payment_status, - "type" => payment.payment_type, - "reference" => payment.reference, - "external_payment_id" => payment.provider_payment_id, - "created_at" => payment.created_at.iso8601 - ) + context "when includes is empty" do + let(:includes) { [] } + + it "serializes the object" do + result = JSON.parse(serializer.to_json) + + expect(result["payment"]).to include( + "lago_id" => payment.id, + "invoice_ids" => payment_request.invoice_ids, + "amount_cents" => payment.amount_cents, + "amount_currency" => payment.amount_currency, + "payment_status" => payment.payable_payment_status, + "type" => payment.payment_type, + "reference" => payment.reference, + "external_payment_id" => payment.provider_payment_id, + "created_at" => payment.created_at.iso8601 + ) + end + end + + context "when includes payment_receipt is set" do + let(:includes) { %i[payment_receipt] } + let!(:payment_receipt) { create(:payment_receipt, payment:) } + + it "includes the payment receipt" do + result = JSON.parse(serializer.to_json) + + expect(result["payment"]["payment_receipt"]).to include( + "lago_id" => payment_receipt.id, + "number" => payment_receipt.number, + "created_at" => payment_receipt.created_at.iso8601 + ) + end end end end