diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 861f07f593f..accd00eb5e1 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -483,6 +483,7 @@ def status_changed_to_finalized? # taxes_rate :float default(0.0), not null # timezone :string default("UTC"), not null # total_amount_cents :bigint default(0), not null +# total_paid_amount_cents :bigint default(0), not null # version_number :integer default(4), not null # voided_at :datetime # created_at :datetime not null diff --git a/app/models/payment.rb b/app/models/payment.rb index 4bc5415fc6d..b926190a852 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -7,11 +7,18 @@ class Payment < ApplicationRecord belongs_to :payable, polymorphic: true belongs_to :payment_provider, optional: true, class_name: 'PaymentProviders::BaseProvider' - belongs_to :payment_provider_customer, class_name: 'PaymentProviderCustomers::BaseCustomer' + belongs_to :payment_provider_customer, optional: true, class_name: 'PaymentProviderCustomers::BaseCustomer' has_many :refunds has_many :integration_resources, as: :syncable + PAYMENT_TYPES = {provider: "provider", manual: "manual"} + attribute :payment_type, :string + enum :payment_type, PAYMENT_TYPES, default: :provider, prefix: :payment_type + validates :payment_type, presence: true + validates :reference, presence: true, length: {maximum: 40}, if: -> { payment_type_manual? } + validates :reference, absence: true, if: -> { payment_type_provider? } + delegate :customer, to: :payable enum payable_payment_status: PAYABLE_PAYMENT_STATUS.map { |s| [s, s] }.to_h @@ -32,7 +39,9 @@ def should_sync_payment? # amount_currency :string not null # payable_payment_status :enum # payable_type :string default("Invoice"), not null +# payment_type :enum default("provider"), not null # provider_payment_data :jsonb +# reference :string # status :string not null # created_at :datetime not null # updated_at :datetime not null @@ -49,6 +58,7 @@ def should_sync_payment? # index_payments_on_payable_type_and_payable_id (payable_type,payable_id) # index_payments_on_payment_provider_customer_id (payment_provider_customer_id) # index_payments_on_payment_provider_id (payment_provider_id) +# index_payments_on_payment_type (payment_type) # # Foreign Keys # diff --git a/db/migrate/20241224141116_add_payment_type_and_reference_to_payments.rb b/db/migrate/20241224141116_add_payment_type_and_reference_to_payments.rb new file mode 100644 index 00000000000..4c51dd8e3d8 --- /dev/null +++ b/db/migrate/20241224141116_add_payment_type_and_reference_to_payments.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class AddPaymentTypeAndReferenceToPayments < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def up + create_enum :payment_type, %w[provider manual] + + safety_assured do + change_table :payments, bulk: true do |t| + t.column :payment_type, :enum, enum_type: 'payment_type', null: true + t.column :reference, :string, default: nil + end + end + + # Backfill existing records + Payment.in_batches(of: 10_000).update_all(payment_type: 'provider') # rubocop:disable Rails/SkipsModelValidations + + safety_assured do + execute <<~SQL + ALTER TABLE payments ALTER COLUMN payment_type SET DEFAULT 'provider'; + SQL + execute <<~SQL + ALTER TABLE payments ALTER COLUMN payment_type SET NOT NULL; + SQL + end + + add_index :payments, :payment_type, algorithm: :concurrently + end + + def down + remove_index :payments, column: :payment_type + + change_table :payments, bulk: true do |t| + t.remove :payment_type + t.remove :reference + end + + drop_enum :payment_type + end +end diff --git a/db/migrate/20241224142141_add_total_paid_amount_cents_to_invoices.rb b/db/migrate/20241224142141_add_total_paid_amount_cents_to_invoices.rb new file mode 100644 index 00000000000..467b3c7a5e3 --- /dev/null +++ b/db/migrate/20241224142141_add_total_paid_amount_cents_to_invoices.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class AddTotalPaidAmountCentsToInvoices < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def up + add_column :invoices, :total_paid_amount_cents, :bigint, null: true + # Backfill + Invoice.in_batches(of: 10_000).each do |batch| + batch.update_all(total_paid_amount_cents: 0) # rubocop:disable Rails/SkipsModelValidations + end + + safety_assured do + execute <<~SQL + ALTER TABLE invoices ALTER COLUMN total_paid_amount_cents SET DEFAULT 0; + SQL + execute <<~SQL + ALTER TABLE invoices ALTER COLUMN total_paid_amount_cents SET NOT NULL; + SQL + end + end + + def down + # Remove the column + remove_column :invoices, :total_paid_amount_cents + end +end diff --git a/db/schema.rb b/db/schema.rb index 610465f71c9..46cad0d3b8c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_12_23_154437) do +ActiveRecord::Schema[7.1].define(version: 2024_12_24_142141) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -23,6 +23,7 @@ create_enum "customer_type", ["company", "individual"] create_enum "inbound_webhook_status", ["pending", "processing", "succeeded", "failed"] create_enum "payment_payable_payment_status", ["pending", "processing", "succeeded", "failed"] + create_enum "payment_type", ["provider", "manual"] create_enum "subscription_invoicing_reason", ["subscription_starting", "subscription_periodic", "subscription_terminating", "in_advance_charge", "in_advance_charge_periodic", "progressive_billing"] create_enum "tax_status", ["pending", "succeeded", "failed"] @@ -934,6 +935,7 @@ t.bigint "negative_amount_cents", default: 0, null: false t.bigint "progressive_billing_credit_amount_cents", default: 0, null: false t.enum "tax_status", enum_type: "tax_status" + t.bigint "total_paid_amount_cents", default: 0, null: false t.index ["customer_id", "sequential_id"], name: "index_invoices_on_customer_id_and_sequential_id", unique: true t.index ["customer_id"], name: "index_invoices_on_customer_id" t.index ["issuing_date"], name: "index_invoices_on_issuing_date" @@ -1115,11 +1117,14 @@ t.uuid "payable_id" t.jsonb "provider_payment_data", default: {} t.enum "payable_payment_status", enum_type: "payment_payable_payment_status" + t.enum "payment_type", default: "provider", null: false, enum_type: "payment_type" + t.string "reference" t.index ["invoice_id"], name: "index_payments_on_invoice_id" t.index ["payable_id", "payable_type"], name: "index_payments_on_payable_id_and_payable_type", unique: true, where: "(payable_payment_status = ANY (ARRAY['pending'::payment_payable_payment_status, 'processing'::payment_payable_payment_status]))" t.index ["payable_type", "payable_id"], name: "index_payments_on_payable_type_and_payable_id" t.index ["payment_provider_customer_id"], name: "index_payments_on_payment_provider_customer_id" t.index ["payment_provider_id"], name: "index_payments_on_payment_provider_id" + t.index ["payment_type"], name: "index_payments_on_payment_type" end create_table "plans", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| diff --git a/spec/factories/payments.rb b/spec/factories/payments.rb index 19de49b1ebb..af4638bc2cf 100644 --- a/spec/factories/payments.rb +++ b/spec/factories/payments.rb @@ -10,6 +10,7 @@ amount_currency { 'EUR' } provider_payment_id { SecureRandom.uuid } status { 'pending' } + payment_type { 'provider' } trait :adyen_payment do association :payment_provider, factory: :adyen_provider diff --git a/spec/models/payment_spec.rb b/spec/models/payment_spec.rb index 1b688f37ef0..bfb31a3a020 100644 --- a/spec/models/payment_spec.rb +++ b/spec/models/payment_spec.rb @@ -3,13 +3,78 @@ require 'rails_helper' RSpec.describe Payment, type: :model do - subject(:payment) { create(:payment) } + subject(:payment) { build(:payment, payment_type:, provider_payment_id:, reference:) } + + let(:payment_type) { 'provider' } + let(:provider_payment_id) { SecureRandom.uuid } + let(:reference) { nil } it_behaves_like 'paper_trail traceable' it { is_expected.to have_many(:integration_resources) } 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) } + + it do + expect(subject) + .to define_enum_for(:payment_type) + .with_values(Payment::PAYMENT_TYPES) + .with_prefix(:payment_type) + .backed_by_column_of_type(:enum) + end + + describe 'validations' do + let(:errors) { payment.errors } + + before { payment.valid? } + + describe 'of reference' do + context 'when payment type is provider' do + context 'when reference is present' do + let(:reference) { '123' } + + it 'adds an error' do + expect(errors.where(:reference, :present)).to be_present + end + end + + context 'when reference is not present' do + it 'does not add an error' do + expect(errors.where(:reference, :present)).not_to be_present + end + end + end + + context 'when payment type is manual' do + let(:payment_type) { 'manual' } + + context 'when reference is not present' do + it 'adds an error' do + expect(errors[:reference]).to include('value_is_mandatory') + end + end + + context 'when reference is present' do + context 'when reference is less than 40 characters' do + let(:reference) { '123' } + + it 'does not add an error' do + expect(errors.where(:reference, :blank)).not_to be_present + end + end + + context 'when reference is more than 40 characters' do + let(:reference) { 'a' * 41 } + + it 'adds an error' do + expect(errors.where(:reference, :too_long)).to be_present + end + end + end + end + end + end describe '#should_sync_payment?' do subject(:method_call) { payment.should_sync_payment? }