diff --git a/app/controllers/letter/batches_controller.rb b/app/controllers/letter/batches_controller.rb index 02e4f10..2d10ad1 100644 --- a/app/controllers/letter/batches_controller.rb +++ b/app/controllers/letter/batches_controller.rb @@ -103,9 +103,12 @@ class Letter::BatchesController < BaseBatchesController end end + hcb_payment_account = current_user.hcb_payment_accounts.find_by(id: letter_batch_params[:hcb_payment_account_id]) + begin @batch.process!( payment_account: payment_account, + hcb_payment_account: hcb_payment_account, us_postage_type: letter_batch_params[:us_postage_type], intl_postage_type: letter_batch_params[:intl_postage_type], template_cycle: letter_batch_params[:template_cycle].to_s.split(",").compact_blank, @@ -219,6 +222,7 @@ class Letter::BatchesController < BaseBatchesController :us_postage_type, :intl_postage_type, :usps_payment_account_id, + :hcb_payment_account_id, :include_qr_code, :print_immediately, :template_cycle, diff --git a/app/models/batch.rb b/app/models/batch.rb index bfcc0e0..6737f3b 100644 --- a/app/models/batch.rb +++ b/app/models/batch.rb @@ -18,6 +18,8 @@ # warehouse_user_facing_title :string # created_at :datetime not null # updated_at :datetime not null +# hcb_payment_account_id :bigint +# hcb_transfer_id :string # letter_mailer_id_id :bigint # letter_queue_id :bigint # letter_return_address_id :bigint @@ -26,6 +28,7 @@ # # Indexes # +# index_batches_on_hcb_payment_account_id (hcb_payment_account_id) # index_batches_on_letter_mailer_id_id (letter_mailer_id_id) # index_batches_on_letter_queue_id (letter_queue_id) # index_batches_on_letter_return_address_id (letter_return_address_id) @@ -36,6 +39,7 @@ # # Foreign Keys # +# fk_rails_... (hcb_payment_account_id => hcb_payment_accounts.id) # fk_rails_... (letter_mailer_id_id => usps_mailer_ids.id) # fk_rails_... (letter_queue_id => letter_queues.id) # fk_rails_... (letter_return_address_id => return_addresses.id) @@ -69,6 +73,7 @@ class Batch < ApplicationRecord self.inheritance_column = "type" belongs_to :user belongs_to :letter_queue, optional: true, class_name: "Letter::Queue" + belongs_to :hcb_payment_account, class_name: "HCB::PaymentAccount", optional: true has_one_attached :csv has_one_attached :labels_pdf has_one_attached :pdf_document diff --git a/app/models/letter/batch.rb b/app/models/letter/batch.rb index 7eed124..03a3d13 100644 --- a/app/models/letter/batch.rb +++ b/app/models/letter/batch.rb @@ -18,6 +18,8 @@ # warehouse_user_facing_title :string # created_at :datetime not null # updated_at :datetime not null +# hcb_payment_account_id :bigint +# hcb_transfer_id :string # letter_mailer_id_id :bigint # letter_queue_id :bigint # letter_return_address_id :bigint @@ -26,6 +28,7 @@ # # Indexes # +# index_batches_on_hcb_payment_account_id (hcb_payment_account_id) # index_batches_on_letter_mailer_id_id (letter_mailer_id_id) # index_batches_on_letter_queue_id (letter_queue_id) # index_batches_on_letter_return_address_id (letter_return_address_id) @@ -36,6 +39,7 @@ # # Foreign Keys # +# fk_rails_... (hcb_payment_account_id => hcb_payment_accounts.id) # fk_rails_... (letter_mailer_id_id => usps_mailer_ids.id) # fk_rails_... (letter_queue_id => letter_queues.id) # fk_rails_... (letter_return_address_id => return_addresses.id) @@ -136,7 +140,7 @@ class Letter::Batch < Batch raise "...we're out of money (ask Nora to put at least #{ActiveSupport::NumberHelper.number_to_currency(indicia_cost)} in the #{options[:payment_account].display_name} account!)" end - purchase_batch_indicia(options[:payment_account]) + purchase_batch_indicia(options[:payment_account], hcb_payment_account: options[:hcb_payment_account]) end # Generate PDF labels with the provided options @@ -151,21 +155,30 @@ class Letter::Batch < Batch end # Purchase indicia for all letters in the batch using a single payment token - def purchase_batch_indicia(payment_account) - # Create a single payment token for the entire batch - payment_token = payment_account.create_payment_token - - # Preload associations to avoid N+1 queries - letters.includes(:address).each do |letter| - next unless letter.postage_type == "indicia" && letter.usps_indicium.nil? - - # Create and purchase indicia for each letter using the same payment token - indicium = USPS::Indicium.new( - letter: letter, - payment_account: payment_account, - mailing_date: letter_mailing_date, + # If hcb_payment_account is provided, creates a single disbursement for the whole batch + def purchase_batch_indicia(usps_payment_account, hcb_payment_account: nil) + if hcb_payment_account.present? + service = HCB::BatchPurchaseService.new( + batch: self, + hcb_payment_account: hcb_payment_account, + usps_payment_account: usps_payment_account, ) - indicium.buy!(payment_token) + unless service.call + raise StandardError, service.errors.join(", ") + end + else + payment_token = usps_payment_account.create_payment_token + + letters.includes(:address).each do |letter| + next unless letter.postage_type == "indicia" && letter.usps_indicium.nil? + + indicium = USPS::Indicium.new( + letter: letter, + payment_account: usps_payment_account, + mailing_date: letter_mailing_date, + ) + indicium.buy!(payment_token) + end end end diff --git a/app/models/warehouse/batch.rb b/app/models/warehouse/batch.rb index ae680e0..1db7881 100644 --- a/app/models/warehouse/batch.rb +++ b/app/models/warehouse/batch.rb @@ -18,6 +18,8 @@ # warehouse_user_facing_title :string # created_at :datetime not null # updated_at :datetime not null +# hcb_payment_account_id :bigint +# hcb_transfer_id :string # letter_mailer_id_id :bigint # letter_queue_id :bigint # letter_return_address_id :bigint @@ -26,6 +28,7 @@ # # Indexes # +# index_batches_on_hcb_payment_account_id (hcb_payment_account_id) # index_batches_on_letter_mailer_id_id (letter_mailer_id_id) # index_batches_on_letter_queue_id (letter_queue_id) # index_batches_on_letter_return_address_id (letter_return_address_id) @@ -36,6 +39,7 @@ # # Foreign Keys # +# fk_rails_... (hcb_payment_account_id => hcb_payment_accounts.id) # fk_rails_... (letter_mailer_id_id => usps_mailer_ids.id) # fk_rails_... (letter_queue_id => letter_queues.id) # fk_rails_... (letter_return_address_id => return_addresses.id) diff --git a/app/services/hcb/batch_purchase_service.rb b/app/services/hcb/batch_purchase_service.rb new file mode 100644 index 0000000..b45ff88 --- /dev/null +++ b/app/services/hcb/batch_purchase_service.rb @@ -0,0 +1,79 @@ +class HCB::BatchPurchaseService + class DisbursementError < StandardError; end + + attr_reader :batch, :hcb_payment_account, :usps_payment_account, :errors + + def initialize(batch:, hcb_payment_account:, usps_payment_account:) + @batch = batch + @hcb_payment_account = hcb_payment_account + @usps_payment_account = usps_payment_account + @errors = [] + end + + def call + return failure("No HCB payment account provided") unless hcb_payment_account + return failure("No USPS payment account provided") unless usps_payment_account + + letters_needing_indicia = batch.letters.select do |letter| + letter.postage_type == "indicia" && letter.usps_indicium.nil? + end + + return true if letters_needing_indicia.empty? + + total_cost_cents = estimate_total_cost_cents(letters_needing_indicia) + + ActiveRecord::Base.transaction do + transfer = create_disbursement!(total_cost_cents) + batch.update!( + hcb_payment_account: hcb_payment_account, + hcb_transfer_id: transfer.id, + ) + + purchase_indicia_for_letters!(letters_needing_indicia) + end + + true + rescue HCBV4::APIError => e + failure("HCB disbursement failed: #{e.message}") + rescue => e + failure("Batch purchase failed: #{e.message}") + end + + private + + def create_disbursement!(amount_cents) + hcb_payment_account.create_disbursement!( + amount_cents: amount_cents, + memo: "Theseus batch postage: #{batch.letters.count} letters", + ) + end + + def purchase_indicia_for_letters!(letters) + payment_token = usps_payment_account.create_payment_token + + letters.each do |letter| + indicium = USPS::Indicium.create!( + letter: letter, + payment_account: usps_payment_account, + hcb_payment_account: hcb_payment_account, + mailing_date: batch.letter_mailing_date, + ) + indicium.buy!(payment_token) + end + end + + def estimate_total_cost_cents(letters) + letters.sum do |letter| + if letter.processing_category == "flat" + 150 + else + 73 + end + end + end + + def failure(message) + @errors << message + false + end +end diff --git a/db/migrate/20251218194622_add_hcb_payment_account_to_batches.rb b/db/migrate/20251218194622_add_hcb_payment_account_to_batches.rb new file mode 100644 index 0000000..966f61b --- /dev/null +++ b/db/migrate/20251218194622_add_hcb_payment_account_to_batches.rb @@ -0,0 +1,6 @@ +class AddHCBPaymentAccountToBatches < ActiveRecord::Migration[8.0] + def change + add_reference :batches, :hcb_payment_account, null: true, foreign_key: true + add_column :batches, :hcb_transfer_id, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 1125044..33b8f58 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[8.0].define(version: 2025_12_18_194459) do +ActiveRecord::Schema[8.0].define(version: 2025_12_18_194622) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_catalog.plpgsql" @@ -106,6 +106,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_18_194459) do t.string "template_cycle", default: [], array: true t.string "letter_return_address_name" t.bigint "letter_queue_id" + t.bigint "hcb_payment_account_id" + t.string "hcb_transfer_id" + t.index ["hcb_payment_account_id"], name: "index_batches_on_hcb_payment_account_id" t.index ["letter_mailer_id_id"], name: "index_batches_on_letter_mailer_id_id" t.index ["letter_queue_id"], name: "index_batches_on_letter_queue_id" t.index ["letter_return_address_id"], name: "index_batches_on_letter_return_address_id" @@ -599,6 +602,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_18_194459) do add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "addresses", "batches" add_foreign_key "api_keys", "users" + add_foreign_key "batches", "hcb_payment_accounts" add_foreign_key "batches", "letter_queues" add_foreign_key "batches", "return_addresses", column: "letter_return_address_id" add_foreign_key "batches", "users"