diff --git a/app/graphql/types/wallets/object.rb b/app/graphql/types/wallets/object.rb index 3b78433f685..9a5aace3f59 100644 --- a/app/graphql/types/wallets/object.rb +++ b/app/graphql/types/wallets/object.rb @@ -18,9 +18,13 @@ class Object < Types::BaseObject field :balance_cents, GraphQL::Types::BigInt, null: false field :consumed_amount_cents, GraphQL::Types::BigInt, null: false + field :ongoing_balance_cents, GraphQL::Types::BigInt, null: false + field :ongoing_usage_balance_cents, GraphQL::Types::BigInt, null: false field :consumed_credits, GraphQL::Types::Float, null: false field :credits_balance, GraphQL::Types::Float, null: false + field :credits_ongoing_balance, GraphQL::Types::Float, null: false + field :credits_ongoing_usage_balance, GraphQL::Types::Float, null: false field :last_balance_sync_at, GraphQL::Types::ISO8601DateTime, null: true field :last_consumed_credit_at, GraphQL::Types::ISO8601DateTime, null: true diff --git a/app/jobs/clock/refresh_wallets_credits_job.rb b/app/jobs/clock/refresh_wallets_credits_job.rb deleted file mode 100644 index a2ae39afab9..00000000000 --- a/app/jobs/clock/refresh_wallets_credits_job.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Clock - class RefreshWalletsCreditsJob < ApplicationJob - queue_as 'clock' - - def perform - Wallet - .active - .joins(customer: :organization) - .merge(Organization.credits_auto_refreshed) - .find_each do |wallet| - Wallets::RefreshCreditsJob.perform_later(wallet) - end - end - end -end diff --git a/app/jobs/clock/refresh_wallets_ongoing_balance_job.rb b/app/jobs/clock/refresh_wallets_ongoing_balance_job.rb new file mode 100644 index 00000000000..4cb330613fe --- /dev/null +++ b/app/jobs/clock/refresh_wallets_ongoing_balance_job.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Clock + class RefreshWalletsOngoingBalanceJob < ApplicationJob + queue_as 'clock' + + def perform + return unless License.premium? + + Wallet.active.find_each do |wallet| + Wallets::RefreshOngoingBalanceJob.perform_later(wallet) + end + end + end +end diff --git a/app/jobs/wallets/refresh_credits_job.rb b/app/jobs/wallets/refresh_ongoing_balance_job.rb similarity index 50% rename from app/jobs/wallets/refresh_credits_job.rb rename to app/jobs/wallets/refresh_ongoing_balance_job.rb index a4b705f5e03..2ae64075528 100644 --- a/app/jobs/wallets/refresh_credits_job.rb +++ b/app/jobs/wallets/refresh_ongoing_balance_job.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true module Wallets - class RefreshCreditsJob < ApplicationJob + class RefreshOngoingBalanceJob < ApplicationJob queue_as 'wallets' def perform(wallet) - Wallets::RefreshCreditsService.call(wallet:) + Wallets::Balance::RefreshOngoingService.call(wallet:) end end end diff --git a/app/models/organization.rb b/app/models/organization.rb index b0b26ae0da0..1ca823c3a0b 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -67,8 +67,6 @@ class Organization < ApplicationRecord after_create :generate_document_number_prefix - scope :credits_auto_refreshed, -> { where(credits_auto_refreshed: true) } - def logo_url return if logo.blank? diff --git a/app/models/wallet.rb b/app/models/wallet.rb index 029b531f8c1..7813f2b1b22 100644 --- a/app/models/wallet.rb +++ b/app/models/wallet.rb @@ -3,14 +3,14 @@ class Wallet < ApplicationRecord include PaperTrailTraceable - belongs_to :customer + belongs_to :customer, -> { with_discarded } has_one :organization, through: :customer has_many :wallet_transactions has_many :recurring_transaction_rules - monetize :balance_cents + monetize :balance_cents, :ongoing_balance_cents, :ongoing_usage_balance_cents monetize :consumed_amount_cents STATUSES = [ diff --git a/app/models/wallet_transaction.rb b/app/models/wallet_transaction.rb index e162c2366bb..29bd970462c 100644 --- a/app/models/wallet_transaction.rb +++ b/app/models/wallet_transaction.rb @@ -25,4 +25,6 @@ class WalletTransaction < ApplicationRecord enum status: STATUSES enum transaction_type: TRANSACTION_TYPES enum source: SOURCES + + scope :pending, -> { where(status: :pending) } end diff --git a/app/serializers/v1/wallet_serializer.rb b/app/serializers/v1/wallet_serializer.rb index 1f6c6e8f72a..ee47fa314b1 100644 --- a/app/serializers/v1/wallet_serializer.rb +++ b/app/serializers/v1/wallet_serializer.rb @@ -12,7 +12,11 @@ def serialize name: model.name, rate_amount: model.rate_amount, credits_balance: model.credits_balance, + credits_ongoing_balance: model.credits_ongoing_balance, + credits_ongoing_usage_balance: model.credits_ongoing_usage_balance, balance_cents: model.balance_cents, + ongoing_balance_cents: model.ongoing_balance_cents, + ongoing_usage_balance_cents: model.ongoing_usage_balance_cents, consumed_credits: model.consumed_credits, created_at: model.created_at&.iso8601, expiration_at: model.expiration_at&.iso8601, diff --git a/app/services/invoices/customer_usage_service.rb b/app/services/invoices/customer_usage_service.rb index fa1662dbf7c..b14c32cd597 100644 --- a/app/services/invoices/customer_usage_service.rb +++ b/app/services/invoices/customer_usage_service.rb @@ -24,6 +24,7 @@ def call return result.not_allowed_failure!(code: 'no_active_subscription') if subscription.blank? result.usage = compute_usage + result.invoice = invoice result end diff --git a/app/services/wallet_transactions/create_service.rb b/app/services/wallet_transactions/create_service.rb index 07b4e00f917..3259ba466e1 100644 --- a/app/services/wallet_transactions/create_service.rb +++ b/app/services/wallet_transactions/create_service.rb @@ -43,6 +43,7 @@ def handle_paid_credits(wallet:, paid_credits:) status: :pending, source:, ) + Wallets::Balance::IncreaseOngoingService.new(wallet:, credits_amount: paid_credits_amount).call BillPaidCreditJob.perform_later( wallet_transaction, diff --git a/app/services/wallets/balance/decrease_ongoing_service.rb b/app/services/wallets/balance/decrease_ongoing_service.rb new file mode 100644 index 00000000000..2e091dd2236 --- /dev/null +++ b/app/services/wallets/balance/decrease_ongoing_service.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Wallets + module Balance + class DecreaseOngoingService < BaseService + def initialize(wallet:, credits_amount:) + super + + @wallet = wallet + @credits_amount = credits_amount + end + + def call + ongoing_usage_balance_cents = wallet.ongoing_usage_balance_cents + + wallet.update!( + ongoing_usage_balance_cents: amount_cents, + credits_ongoing_usage_balance: credits_amount, + ongoing_balance_cents:, + credits_ongoing_balance:, + ) + + handle_threshold_top_up(ongoing_usage_balance_cents) + + result.wallet = wallet + result + end + + private + + attr_reader :wallet, :credits_amount + + def handle_threshold_top_up(ongoing_usage_balance_cents) + threshold_rule = wallet.recurring_transaction_rules.where(rule_type: :threshold).first + + return if threshold_rule.nil? || wallet.credits_ongoing_balance > threshold_rule.threshold_credits + return if ongoing_usage_balance_cents == amount_cents + + WalletTransactions::CreateJob.set(wait: 2.seconds).perform_later( + organization_id: wallet.organization.id, + wallet_id: wallet.id, + paid_credits: threshold_rule.paid_credits.to_s, + granted_credits: threshold_rule.granted_credits.to_s, + source: :threshold, + ) + end + + def currency + @currency ||= wallet.ongoing_balance.currency + end + + def amount_cents + @amount_cents ||= wallet.rate_amount * credits_amount * currency.subunit_to_unit + end + + def pending_transactions + @pending_transactions ||= wallet.wallet_transactions.pending + end + + def ongoing_balance_cents + [ + 0, + (pending_transactions.sum(:amount) * currency.subunit_to_unit) - amount_cents + wallet.balance_cents, + ].max + end + + def credits_ongoing_balance + [ + 0, + pending_transactions.sum(:credit_amount) - credits_amount + wallet.credits_balance, + ].max + end + end + end +end diff --git a/app/services/wallets/balance/decrease_service.rb b/app/services/wallets/balance/decrease_service.rb index bd73f7eaba6..19165621d51 100644 --- a/app/services/wallets/balance/decrease_service.rb +++ b/app/services/wallets/balance/decrease_service.rb @@ -23,7 +23,7 @@ def call last_consumed_credit_at: Time.current, ) - handle_threshold_top_up + Wallets::Balance::RefreshOngoingService.call(wallet:) result.wallet = wallet result @@ -32,20 +32,6 @@ def call private attr_reader :wallet, :credits_amount - - def handle_threshold_top_up - threshold_rule = wallet.recurring_transaction_rules.where(rule_type: :threshold).first - - return if threshold_rule.nil? || wallet.credits_balance > threshold_rule.threshold_credits - - WalletTransactions::CreateJob.set(wait: 2.seconds).perform_later( - organization_id: wallet.organization.id, - wallet_id: wallet.id, - paid_credits: threshold_rule.paid_credits.to_s, - granted_credits: threshold_rule.granted_credits.to_s, - source: :threshold, - ) - end end end end diff --git a/app/services/wallets/balance/increase_ongoing_service.rb b/app/services/wallets/balance/increase_ongoing_service.rb new file mode 100644 index 00000000000..fe6661d5fd4 --- /dev/null +++ b/app/services/wallets/balance/increase_ongoing_service.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Wallets + module Balance + class IncreaseOngoingService < BaseService + def initialize(wallet:, credits_amount:) + super(nil) + + @wallet = wallet + @credits_amount = credits_amount + end + + def call + currency = wallet.ongoing_balance.currency + amount_cents = wallet.rate_amount * credits_amount * currency.subunit_to_unit + + update_params = { + ongoing_balance_cents: wallet.ongoing_balance_cents + amount_cents, + credits_ongoing_balance: wallet.credits_ongoing_balance + credits_amount, + } + + wallet.update!(update_params) + + result.wallet = wallet + result + end + + private + + attr_reader :wallet, :credits_amount + end + end +end diff --git a/app/services/wallets/balance/increase_service.rb b/app/services/wallets/balance/increase_service.rb index 254dc66e3ca..84e1c4464fe 100644 --- a/app/services/wallets/balance/increase_service.rb +++ b/app/services/wallets/balance/increase_service.rb @@ -4,7 +4,7 @@ module Wallets module Balance class IncreaseService < BaseService def initialize(wallet:, credits_amount:, reset_consumed_credits: false) - super(nil) + super @wallet = wallet @credits_amount = credits_amount @@ -27,6 +27,7 @@ def call end wallet.update!(update_params) + Wallets::Balance::RefreshOngoingService.call(wallet:) result.wallet = wallet result diff --git a/app/services/wallets/balance/refresh_ongoing_service.rb b/app/services/wallets/balance/refresh_ongoing_service.rb new file mode 100644 index 00000000000..3b6971bfea3 --- /dev/null +++ b/app/services/wallets/balance/refresh_ongoing_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Wallets + module Balance + class RefreshOngoingService < BaseService + def initialize(wallet:) + @wallet = wallet + super + end + + def call + total_amount = customer.active_subscriptions.sum do |subscription| + ::Invoices::CustomerUsageService.call( + nil, # current_user + customer_id: customer.external_id, + subscription_id: subscription.external_id, + organization_id: customer.organization_id, + ).invoice.total_amount + end + credits_amount = total_amount.to_f.fdiv(wallet.rate_amount) + Wallets::Balance::DecreaseOngoingService.call(wallet:, credits_amount:).raise_if_error! + + result.wallet = wallet + result + end + + private + + attr_reader :wallet + + delegate :customer, to: :wallet + end + end +end diff --git a/app/services/wallets/refresh_credits_service.rb b/app/services/wallets/refresh_credits_service.rb deleted file mode 100644 index c516abe575c..00000000000 --- a/app/services/wallets/refresh_credits_service.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -module Wallets - class RefreshCreditsService < BaseService - def initialize(wallet:) - @wallet = wallet - super - end - - def call - # TODO - end - end -end diff --git a/clock.rb b/clock.rb index bd4d909d458..5ecf4528ab0 100644 --- a/clock.rb +++ b/clock.rb @@ -26,8 +26,8 @@ module Clockwork Clock::RefreshDraftInvoicesJob.perform_later end - every(5.minutes, 'schedule:refresh_wallets_credits') do - Clock::RefreshWalletsCreditsJob.perform_later + every(5.minutes, 'schedule:refresh_wallets_ongoing_balance') do + Clock::RefreshWalletsOngoingBalanceJob.perform_later end every(1.hour, 'schedule:terminate_ended_subscriptions', at: '*:05') do diff --git a/db/migrate/20240118135350_remove_credits_auto_refreshed_from_organizations.rb b/db/migrate/20240118135350_remove_credits_auto_refreshed_from_organizations.rb new file mode 100644 index 00000000000..5c8cf821653 --- /dev/null +++ b/db/migrate/20240118135350_remove_credits_auto_refreshed_from_organizations.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class RemoveCreditsAutoRefreshedFromOrganizations < ActiveRecord::Migration[7.0] + def change + remove_column :organizations, :credits_auto_refreshed, :boolean + + change_table :wallets, bulk: true do |t| + t.bigint :ongoing_balance_cents, default: 0, null: false + t.bigint :ongoing_usage_balance_cents, default: 0, null: false + + t.decimal :credits_ongoing_balance, precision: 30, scale: 5, default: '0.0', null: false + t.decimal :credits_ongoing_usage_balance, precision: 30, scale: 5, default: '0.0', null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 91836a3bdad..719779b0984 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -671,11 +671,10 @@ t.string "tax_identification_number" t.integer "net_payment_term", default: 0, null: false t.string "default_currency", default: "USD", null: false + t.boolean "eu_tax_management", default: false t.integer "document_numbering", default: 0, null: false t.string "document_number_prefix" - t.boolean "eu_tax_management", default: false t.boolean "clickhouse_aggregation", default: false, null: false - t.boolean "credits_auto_refreshed", default: false, null: false t.index ["api_key"], name: "index_organizations_on_api_key", unique: true t.check_constraint "invoice_grace_period >= 0", name: "check_organizations_on_invoice_grace_period" t.check_constraint "net_payment_term >= 0", name: "check_organizations_on_net_payment_term" @@ -902,6 +901,10 @@ t.string "balance_currency", null: false t.bigint "consumed_amount_cents", default: 0, null: false t.string "consumed_amount_currency", null: false + t.bigint "ongoing_balance_cents", default: 0, null: false + t.bigint "ongoing_usage_balance_cents", default: 0, null: false + t.decimal "credits_ongoing_balance", precision: 30, scale: 5, default: "0.0", null: false + t.decimal "credits_ongoing_usage_balance", precision: 30, scale: 5, default: "0.0", null: false t.index ["customer_id"], name: "index_wallets_on_customer_id" end diff --git a/db/seeds/base.rb b/db/seeds/base.rb index 063b97e9f1c..4ed3b67adc1 100644 --- a/db/seeds/base.rb +++ b/db/seeds/base.rb @@ -188,7 +188,11 @@ status: :active, rate_amount: BigDecimal('1.00'), balance: BigDecimal('100.00'), + ongoing_balance: BigDecimal('100.00'), + ongoing_usage_balance: BigDecimal('100.00'), credits_balance: BigDecimal('100.00'), + credits_ongoing_balance: BigDecimal('100.00'), + credits_ongoing_usage_balance: BigDecimal('100.00'), currency: 'EUR', ) @@ -199,7 +203,11 @@ terminated_at: Time.zone.now - 3.days, rate_amount: BigDecimal('1.00'), balance: BigDecimal('86.00'), + ongoing_balance: BigDecimal('86.00'), + ongoing_usage_balance: BigDecimal('86.00'), credits_balance: BigDecimal('86.00'), + credits_ongoing_balance: BigDecimal('86.00'), + credits_ongoing_usage_balance: BigDecimal('86.00'), consumed_credits: BigDecimal('114.00'), currency: 'EUR', ) diff --git a/schema.graphql b/schema.graphql index 15b670fe84d..cba656801b9 100644 --- a/schema.graphql +++ b/schema.graphql @@ -6069,6 +6069,8 @@ type Wallet { consumedCredits: Float! createdAt: ISO8601DateTime! creditsBalance: Float! + creditsOngoingBalance: Float! + creditsOngoingUsageBalance: Float! currency: CurrencyEnum! customer: Customer expirationAt: ISO8601DateTime @@ -6076,6 +6078,8 @@ type Wallet { lastBalanceSyncAt: ISO8601DateTime lastConsumedCreditAt: ISO8601DateTime name: String + ongoingBalanceCents: BigInt! + ongoingUsageBalanceCents: BigInt! rateAmount: Float! recurringTransactionRules: [RecurringTransactionRule!] status: WalletStatusEnum! diff --git a/schema.json b/schema.json index a3847cb953b..6a82fc50b85 100644 --- a/schema.json +++ b/schema.json @@ -27862,6 +27862,42 @@ ] }, + { + "name": "creditsOngoingBalance", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "creditsOngoingUsageBalance", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, { "name": "currency", "description": null, @@ -27968,6 +28004,42 @@ ] }, + { + "name": "ongoingBalanceCents", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "BigInt", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, + { + "name": "ongoingUsageBalanceCents", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "BigInt", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null, + "args": [ + + ] + }, { "name": "rateAmount", "description": null, diff --git a/spec/graphql/types/wallets/object_spec.rb b/spec/graphql/types/wallets/object_spec.rb new file mode 100644 index 00000000000..636817a4f9d --- /dev/null +++ b/spec/graphql/types/wallets/object_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Types::Wallets::Object do + subject { described_class } + + it { is_expected.to have_field(:customer).of_type('Customer') } + + it { is_expected.to have_field(:currency).of_type('CurrencyEnum!') } + it { is_expected.to have_field(:name).of_type('String') } + it { is_expected.to have_field(:status).of_type('WalletStatusEnum!') } + + it { is_expected.to have_field(:rate_amount).of_type('Float!') } + + it { is_expected.to have_field(:balance_cents).of_type('BigInt!') } + it { is_expected.to have_field(:consumed_amount_cents).of_type('BigInt!') } + it { is_expected.to have_field(:ongoing_balance_cents).of_type('BigInt!') } + it { is_expected.to have_field(:ongoing_usage_balance_cents).of_type('BigInt!') } + + it { is_expected.to have_field(:consumed_credits).of_type('Float!') } + it { is_expected.to have_field(:credits_balance).of_type('Float!') } + it { is_expected.to have_field(:credits_ongoing_balance).of_type('Float!') } + it { is_expected.to have_field(:credits_ongoing_usage_balance).of_type('Float!') } + + it { is_expected.to have_field(:last_balance_sync_at).of_type('ISO8601DateTime') } + it { is_expected.to have_field(:last_consumed_credit_at).of_type('ISO8601DateTime') } + + it { is_expected.to have_field(:recurring_transaction_rules).of_type('[RecurringTransactionRule!]') } + + it { is_expected.to have_field(:created_at).of_type('ISO8601DateTime!') } + it { is_expected.to have_field(:expiration_at).of_type('ISO8601DateTime') } + it { is_expected.to have_field(:terminated_at).of_type('ISO8601DateTime') } + it { is_expected.to have_field(:updated_at).of_type('ISO8601DateTime!') } +end diff --git a/spec/jobs/clock/refresh_wallets_credits_job_spec.rb b/spec/jobs/clock/refresh_wallets_credits_job_spec.rb index a9b0f7200d8..9304ec4b165 100644 --- a/spec/jobs/clock/refresh_wallets_credits_job_spec.rb +++ b/spec/jobs/clock/refresh_wallets_credits_job_spec.rb @@ -2,39 +2,41 @@ require 'rails_helper' -describe Clock::RefreshWalletsCreditsJob, job: true do +describe Clock::RefreshWalletsOngoingBalanceJob, job: true do subject { described_class } describe '.perform' do - let(:organization) { create(:organization, credits_auto_refreshed: true) } + let(:organization) { create(:organization) } let(:customer) { create(:customer, organization:) } let(:wallet) { create(:wallet, customer:) } before do wallet - allow(Wallets::RefreshCreditsService).to receive(:call) + allow(Wallets::Balance::RefreshOngoingService).to receive(:call) end - it 'calls the refresh service' do - described_class.perform_now - expect(Wallets::RefreshCreditsJob).to have_been_enqueued.with(wallet) - end - - context 'when not active' do - let(:wallet) { create(:wallet, :terminated) } - + context 'when freemium' do it 'does not call the refresh service' do described_class.perform_now - expect(Wallets::RefreshCreditsJob).not_to have_been_enqueued.with(wallet) + expect(Wallets::RefreshOngoingBalanceJob).not_to have_been_enqueued.with(wallet) end end - context 'when not credits_auto_refreshed' do - let(:organization) { create(:organization, credits_auto_refreshed: false) } + context 'when premium' do + around { |test| lago_premium!(&test) } - it 'does not call the refresh service' do + it 'calls the refresh service' do described_class.perform_now - expect(Wallets::RefreshCreditsJob).not_to have_been_enqueued.with(wallet) + expect(Wallets::RefreshOngoingBalanceJob).to have_been_enqueued.with(wallet) + end + + context 'when not active' do + let(:wallet) { create(:wallet, :terminated) } + + it 'does not call the refresh service' do + described_class.perform_now + expect(Wallets::RefreshOngoingBalanceJob).not_to have_been_enqueued.with(wallet) + end end end end diff --git a/spec/jobs/wallets/refresh_credits_job_spec.rb b/spec/jobs/wallets/refresh_credits_job_spec.rb deleted file mode 100644 index 2c9ca988f18..00000000000 --- a/spec/jobs/wallets/refresh_credits_job_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Wallets::RefreshCreditsJob, type: :job do - let(:wallet) { create(:wallet) } - let(:result) { BaseService::Result.new } - - let(:refresh_service) do - instance_double(Wallets::RefreshCreditsService) - end - - it 'delegates to the RefreshCredits service' do - allow(Wallets::RefreshCreditsService).to receive(:new).with(wallet:).and_return(refresh_service) - allow(refresh_service).to receive(:call).and_return(result) - - described_class.perform_now(wallet) - - expect(Wallets::RefreshCreditsService).to have_received(:new) - expect(refresh_service).to have_received(:call) - end -end diff --git a/spec/jobs/wallets/refresh_ongoing_balance_job_spec.rb b/spec/jobs/wallets/refresh_ongoing_balance_job_spec.rb new file mode 100644 index 00000000000..103e47e3f12 --- /dev/null +++ b/spec/jobs/wallets/refresh_ongoing_balance_job_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Wallets::RefreshOngoingBalanceJob, type: :job do + let(:wallet) { create(:wallet) } + let(:result) { BaseService::Result.new } + + let(:refresh_service) do + instance_double(Wallets::Balance::RefreshOngoingService) + end + + it 'delegates to the RefreshOngoingBalance service' do + allow(Wallets::Balance::RefreshOngoingService).to receive(:new).with(wallet:).and_return(refresh_service) + allow(refresh_service).to receive(:call).and_return(result) + + described_class.perform_now(wallet) + + expect(Wallets::Balance::RefreshOngoingService).to have_received(:new) + expect(refresh_service).to have_received(:call) + end +end diff --git a/spec/serializers/v1/wallet_serializer_spec.rb b/spec/serializers/v1/wallet_serializer_spec.rb index 18e7274ab2d..98587482d3d 100644 --- a/spec/serializers/v1/wallet_serializer_spec.rb +++ b/spec/serializers/v1/wallet_serializer_spec.rb @@ -26,6 +26,10 @@ 'terminated_at' => wallet.terminated_at, 'credits_balance' => wallet.credits_balance.to_s, 'balance_cents' => wallet.balance_cents, + 'credits_ongoing_balance' => wallet.credits_ongoing_balance.to_s, + 'credits_ongoing_usage_balance' => wallet.credits_ongoing_usage_balance.to_s, + 'ongoing_balance_cents' => wallet.ongoing_balance_cents, + 'ongoing_usage_balance_cents' => wallet.ongoing_usage_balance_cents, 'consumed_credits' => wallet.consumed_credits.to_s, ) diff --git a/spec/services/invoices/customer_usage_service_spec.rb b/spec/services/invoices/customer_usage_service_spec.rb index cc971ac36bd..88407b26abe 100644 --- a/spec/services/invoices/customer_usage_service_spec.rb +++ b/spec/services/invoices/customer_usage_service_spec.rb @@ -81,18 +81,19 @@ aggregate_failures do expect(result).to be_success - - expect(result.usage.id).to be_nil - expect(result.usage.from_datetime).to eq(Time.current.beginning_of_month.iso8601) - expect(result.usage.to_datetime).to eq(Time.current.end_of_month.iso8601) - expect(result.usage.issuing_date).to eq(Time.zone.today.end_of_month.iso8601) + expect(result.invoice).to be_a(Invoice) + + expect(result.usage).to have_attributes( + from_datetime: Time.current.beginning_of_month.iso8601, + to_datetime: Time.current.end_of_month.iso8601, + issuing_date: Time.zone.today.end_of_month.iso8601, + currency: 'EUR', + amount_cents: 2532, # 1266 * 2, + taxes_amount_cents: 506, # 1266 * 2 * 0.2 = 506.4 + total_amount_cents: 3038, + ) expect(result.usage.fees.size).to eq(1) expect(result.usage.fees.first.charge.invoice_display_name).to eq(charge.invoice_display_name) - - expect(result.usage.currency).to eq('EUR') - expect(result.usage.amount_cents).to eq(2532) # 1266 * 2 - expect(result.usage.taxes_amount_cents).to eq(506) # 1266 * 2 * 0.2 = 506.4 - expect(result.usage.total_amount_cents).to eq(3038) end end @@ -134,17 +135,19 @@ aggregate_failures do expect(result).to be_success + expect(result.invoice).to be_a(Invoice) + + expect(result.usage).to have_attributes( + issuing_date: '2022-07-06', + currency: 'EUR', + amount_cents: 2532, # 1266 * 2, + taxes_amount_cents: 506, # 1266 * 2 * 0.2 = 506.4 + total_amount_cents: 3038, + ) - expect(result.usage.id).to be_nil expect(result.usage.from_datetime.to_date.to_s).to eq('2022-06-07') expect(result.usage.to_datetime.to_date.to_s).to eq('2022-07-06') - expect(result.usage.issuing_date).to eq('2022-07-06') expect(result.usage.fees.size).to eq(1) - - expect(result.usage.currency).to eq('EUR') - expect(result.usage.amount_cents).to eq(2532) # 1266 * 2 - expect(result.usage.taxes_amount_cents).to eq(506) # 1266 * 2 * 0.2 = 506.4 - expect(result.usage.total_amount_cents).to eq(3038) end end end diff --git a/spec/services/wallet_transactions/create_service_spec.rb b/spec/services/wallet_transactions/create_service_spec.rb index 003c657b214..2b920147064 100644 --- a/spec/services/wallet_transactions/create_service_spec.rb +++ b/spec/services/wallet_transactions/create_service_spec.rb @@ -9,7 +9,16 @@ let(:organization) { membership.organization } let(:customer) { create(:customer, organization:) } let(:subscription) { create(:subscription, customer:) } - let(:wallet) { create(:wallet, customer:, balance_cents: 1000, credits_balance: 10.0) } + let(:wallet) do + create( + :wallet, + customer:, + balance_cents: 1000, + credits_balance: 10.0, + ongoing_balance_cents: 1000, + credits_ongoing_balance: 10.0, + ) + end before do subscription @@ -56,6 +65,13 @@ expect(wallet.reload.credits_balance).to eq(25.0) end + it 'updates wallet ongoing balance with granted and paid credits' do + create_service.create(**create_args) + + expect(wallet.reload.ongoing_balance_cents).to eq(3500) + expect(wallet.reload.credits_ongoing_balance).to eq(35.0) + end + context 'with validation error' do let(:paid_credits) { '-15.00' } diff --git a/spec/services/wallets/balance/decrease_ongoing_service_spec.rb b/spec/services/wallets/balance/decrease_ongoing_service_spec.rb new file mode 100644 index 00000000000..3c3efc917ca --- /dev/null +++ b/spec/services/wallets/balance/decrease_ongoing_service_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Wallets::Balance::DecreaseOngoingService, type: :service do + subject(:decrease_service) { described_class.new(wallet:, credits_amount:) } + + let(:wallet) do + create( + :wallet, + balance_cents: 1000, + ongoing_balance_cents: 800, + ongoing_usage_balance_cents: 200, + credits_balance: 10.0, + credits_ongoing_balance: 8.0, + credits_ongoing_usage_balance: 2.0, + ) + end + let(:credits_amount) { BigDecimal('4.5') } + + before { wallet } + + describe '.call' do + it 'updates wallet balance' do + expect { decrease_service.call } + .to change(wallet.reload, :ongoing_usage_balance_cents).from(200).to(450) + .and change(wallet, :credits_ongoing_usage_balance).from(2.0).to(4.5) + .and change(wallet, :ongoing_balance_cents).from(800).to(550) + .and change(wallet, :credits_ongoing_balance).from(8.0).to(5.5) + end + + context 'with recurring transaction threshold rule' do + let(:recurring_transaction_rule) do + create(:recurring_transaction_rule, wallet:, rule_type: 'threshold', threshold_credits: '6.0') + end + + before { recurring_transaction_rule } + + it 'calls wallet transaction create job when threshold border has been crossed' do + expect { decrease_service.call }.to have_enqueued_job(WalletTransactions::CreateJob) + end + + context 'when border has NOT been crossed' do + let(:recurring_transaction_rule) do + create(:recurring_transaction_rule, wallet:, rule_type: 'threshold', threshold_credits: '2.0') + end + + it 'does not call wallet transaction create job' do + expect { decrease_service.call }.not_to have_enqueued_job(WalletTransactions::CreateJob) + end + end + end + end +end diff --git a/spec/services/wallets/balance/decrease_service_spec.rb b/spec/services/wallets/balance/decrease_service_spec.rb index 2c56fea8c92..9626aa34c80 100644 --- a/spec/services/wallets/balance/decrease_service_spec.rb +++ b/spec/services/wallets/balance/decrease_service_spec.rb @@ -5,7 +5,15 @@ RSpec.describe Wallets::Balance::DecreaseService, type: :service do subject(:create_service) { described_class.new(wallet:, credits_amount:) } - let(:wallet) { create(:wallet, balance_cents: 1000, credits_balance: 10.0) } + let(:wallet) do + create( + :wallet, + balance_cents: 1000, + ongoing_balance_cents: 800, + credits_balance: 10.0, + credits_ongoing_balance: 8.0, + ) + end let(:credits_amount) { BigDecimal('4.5') } before { wallet } @@ -23,26 +31,10 @@ .and change(wallet, :consumed_amount_cents).from(0).to(450) end - context 'with recurring transaction threshold rule' do - let(:recurring_transaction_rule) do - create(:recurring_transaction_rule, wallet:, rule_type: 'threshold', threshold_credits: '6.0') - end - - before { recurring_transaction_rule } - - it 'calls wallet transaction create job when threshold border has been crossed' do - expect { create_service.call }.to have_enqueued_job(WalletTransactions::CreateJob) - end - - context 'when border has NOT been crossed' do - let(:recurring_transaction_rule) do - create(:recurring_transaction_rule, wallet:, rule_type: 'threshold', threshold_credits: '2.0') - end - - it 'does not call wallet transaction create job' do - expect { create_service.call }.not_to have_enqueued_job(WalletTransactions::CreateJob) - end - end + it 'refreshes wallet ongoing balance' do + expect { create_service.call } + .to change(wallet.reload, :ongoing_balance_cents).from(800).to(550) + .and change(wallet, :credits_ongoing_balance).from(8.0).to(5.5) end end end diff --git a/spec/services/wallets/balance/increase_ongoing_service_spec.rb b/spec/services/wallets/balance/increase_ongoing_service_spec.rb new file mode 100644 index 00000000000..652df5bbbf2 --- /dev/null +++ b/spec/services/wallets/balance/increase_ongoing_service_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Wallets::Balance::IncreaseOngoingService, type: :service do + subject(:create_service) { described_class.new(wallet:, credits_amount:) } + + let(:credits_amount) { BigDecimal('4.5') } + let(:wallet) do + create( + :wallet, + balance_cents: 1000, + ongoing_balance_cents: 800, + credits_balance: 10.0, + credits_ongoing_balance: 8.0, + ) + end + + before { wallet } + + describe '.call' do + it 'updates wallet ongoing balance' do + expect { create_service.call } + .to change(wallet, :ongoing_balance_cents).from(800).to(1250) + .and change(wallet, :credits_ongoing_balance).from(8.0).to(12.5) + end + end +end diff --git a/spec/services/wallets/balance/increase_service_spec.rb b/spec/services/wallets/balance/increase_service_spec.rb index 0958e1dd2ab..26e02241d78 100644 --- a/spec/services/wallets/balance/increase_service_spec.rb +++ b/spec/services/wallets/balance/increase_service_spec.rb @@ -5,8 +5,16 @@ RSpec.describe Wallets::Balance::IncreaseService, type: :service do subject(:create_service) { described_class.new(wallet:, credits_amount:) } - let(:wallet) { create(:wallet, balance_cents: 1000, credits_balance: 10.0) } let(:credits_amount) { BigDecimal('4.5') } + let(:wallet) do + create( + :wallet, + balance_cents: 1000, + ongoing_balance_cents: 800, + credits_balance: 10.0, + credits_ongoing_balance: 8.0, + ) + end before { wallet } @@ -16,5 +24,11 @@ .to change(wallet.reload, :balance_cents).from(1000).to(1450) .and change(wallet, :credits_balance).from(10.0).to(14.5) end + + it 'refreshes wallet ongoing balance' do + expect { create_service.call } + .to change(wallet.reload, :ongoing_balance_cents).from(800).to(1450) + .and change(wallet, :credits_ongoing_balance).from(8.0).to(14.5) + end end end diff --git a/spec/services/wallets/balance/refresh_ongoing_service_spec.rb b/spec/services/wallets/balance/refresh_ongoing_service_spec.rb new file mode 100644 index 00000000000..b386be4dd79 --- /dev/null +++ b/spec/services/wallets/balance/refresh_ongoing_service_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Wallets::Balance::RefreshOngoingService, type: :service do + subject(:refresh_service) { described_class.new(wallet:) } + + let(:wallet) do + create( + :wallet, + customer:, + balance_cents: 1000, + ongoing_balance_cents: 800, + ongoing_usage_balance_cents: 200, + credits_balance: 10.0, + credits_ongoing_balance: 8.0, + credits_ongoing_usage_balance: 2.0, + ) + end + + let(:membership) { create(:membership) } + let(:organization) { membership.organization } + let(:customer) { create(:customer, organization:) } + let(:first_subscription) do + create(:subscription, organization:, customer:, started_at: Time.zone.now - 2.years) + end + let(:second_subscription) do + create(:subscription, organization:, customer:, started_at: Time.zone.now - 1.year) + end + let(:timestamp) { Time.current } + let(:billable_metric) { create(:billable_metric, aggregation_type: 'count_agg') } + + let(:first_charge) do + create( + :standard_charge, + plan: first_subscription.plan, + billable_metric:, + properties: { amount: '3' }, + ) + end + let(:second_charge) do + create( + :standard_charge, + plan: second_subscription.plan, + billable_metric:, + properties: { amount: '5' }, + ) + end + + let(:events) do + create_list( + :event, + 2, + organization: wallet.organization, + subscription: first_subscription, + customer: first_subscription.customer, + code: billable_metric.code, + timestamp:, + ).push( + create( + :event, + organization: wallet.organization, + subscription: second_subscription, + customer: second_subscription.customer, + code: billable_metric.code, + timestamp:, + ), + ) + end + + before do + first_charge + second_charge + wallet + events + end + + describe '.call' do + it 'updates wallet ongoing balance' do + expect { refresh_service.call } + .to change(wallet.reload, :ongoing_usage_balance_cents).from(200).to(1100) + .and change(wallet, :credits_ongoing_usage_balance).from(2.0).to(11.0) + .and change(wallet, :ongoing_balance_cents).from(800).to(0) + .and change(wallet, :credits_ongoing_balance).from(8.0).to(0) + end + + it 'returns the wallet' do + expect(refresh_service.call.wallet).to eq(wallet) + end + end +end