# == Schema Information # # Table name: letters # # id :bigint not null, primary key # aasm_state :string # body :text # height :decimal(, ) # idempotency_key :string # imb_rollover_count :integer # imb_serial_number :integer # mailed_at :datetime # mailing_date :date # metadata :jsonb # non_machinable :boolean # postage :decimal(, ) # postage_type :integer # printed_at :datetime # processing_category :integer # received_at :datetime # recipient_email :string # return_address_name :string # rubber_stamps :text # tags :citext default([]), is an Array # user_facing_title :string # weight :decimal(, ) # width :decimal(, ) # created_at :datetime not null # updated_at :datetime not null # address_id :bigint not null # batch_id :bigint # letter_queue_id :bigint # return_address_id :bigint not null # user_id :bigint not null # usps_mailer_id_id :bigint not null # # Indexes # # index_letters_on_address_id (address_id) # index_letters_on_batch_id (batch_id) # index_letters_on_idempotency_key (idempotency_key) UNIQUE # index_letters_on_imb_serial_number (imb_serial_number) # index_letters_on_letter_queue_id (letter_queue_id) # index_letters_on_return_address_id (return_address_id) # index_letters_on_tags (tags) USING gin # index_letters_on_user_id (user_id) # index_letters_on_usps_mailer_id_id (usps_mailer_id_id) # # Foreign Keys # # fk_rails_... (address_id => addresses.id) # fk_rails_... (batch_id => batches.id) # fk_rails_... (letter_queue_id => letter_queues.id) # fk_rails_... (return_address_id => return_addresses.id) # fk_rails_... (user_id => users.id) # fk_rails_... (usps_mailer_id_id => usps_mailer_ids.id) # class Letter < ApplicationRecord include PublicIdentifiable set_public_id_prefix "ltr" include HasAddress include CanBeBatched include AASM include Taggable # Add ActiveStorage attachment for the label PDF has_one_attached :label belongs_to :return_address, optional: true has_many :iv_mtr_events, class_name: "USPS::IVMTR::Event" belongs_to :user belongs_to :queue, class_name: "Letter::Queue", foreign_key: "letter_queue_id", optional: true aasm timestamps: true do state :queued state :pending, initial: true state :printed state :mailed state :received event :batch_from_queue do transitions from: :queued, to: :pending end event :mark_printed do transitions from: :pending, to: :printed end event :mark_mailed do transitions from: [:pending, :printed], to: :mailed end event :mark_received do transitions from: :mailed, to: :received end event :unreceive do transitions from: :received, to: :mailed end end def display_name = user_facing_title || tags.compact_blank.join(", ") || public_id def return_address_name_line = return_address_name.presence || return_address&.name def been_mailed? = mailed? || received? belongs_to :usps_mailer_id, class_name: "USPS::MailerId" after_create :set_imb_sequence # Generate a label for this letter def generate_label(options = {}) pdf = SnailMail::PhlexService.generate_label(self, options) # Directly attach the PDF to this letter attach_pdf(pdf.render) # Save the record to persist the attachment save end # Directly attach a PDF to this letter def attach_pdf(pdf_data) io = StringIO.new(pdf_data) label.attach( io: io, filename: "label_#{Time.now.to_i}.pdf", content_type: "application/pdf", ) end def flirt desired_price = USPS::PricingEngine.fcmi_price( processing_category, weight, address.country ) USPS::FLIRTEngine.closest_us_price(desired_price) end def self.find_by_imb_sn(imb_sn, mailer_id = nil) query = where(imb_serial_number: imb_sn.to_i) query = query.where(usps_mailer_id: mailer_id) if mailer_id query.order(imb_rollover_count: :desc).first end enum :processing_category, { letter: 0, flat: 1, }, instance_methods: false, prefix: true, suffix: true enum :postage_type, { stamps: 0, indicia: 1, international_origin: 2, }, instance_methods: false has_one :usps_indicium, class_name: "USPS::Indicium" attribute :mailing_date, :date validates :mailing_date, presence: true, if: -> { postage_type == "indicia" } validate :mailing_date_not_in_past, if: -> { mailing_date.present? }, on: :create validates :processing_category, presence: true validate :validate_postage_type_by_return_address before_save :set_postage def mailing_date_not_in_past if mailing_date < Date.current errors.add(:mailing_date, "cannot be in the past") end end def validate_postage_type_by_return_address if return_address.present? && postage_type.present? if return_address.us? if postage_type == "international_origin" errors.add(:postage_type, "cannot be international origin when return address is in the US") end else if postage_type != "international_origin" errors.add(:postage_type, "must be international origin when return address is not in the US") end end end end def default_mailing_date now = Time.current today = now.to_date # If it's before 4PM on a business day, default to today if now.hour < 16 && today.on_weekday? today else # Otherwise, default to next business day next_business_day = today loop do next_business_day += 1 break if next_business_day.on_weekday? end next_business_day end end alias_method :to_param, :public_id def events iv = iv_mtr_events.map do |event| e = event.hydrated { happened_at: event.happened_at.in_time_zone("America/New_York"), source: "USPS IV-MTR", location: "#{e.scan_facility_city}, #{e.scan_facility_state} #{e.scan_facility_zip}", facility: "#{e.scan_facility_name} (#{e.scan_locale_key})", description: "[OP#{e.opcode.code}] #{e.opcode.process_description}", extra_info: "#{e.handling_event_type_description} – #{e.mail_phase} – #{e.machine_name} (#{event.payload.dig("machineId") || "no ID"})", } end timestamps = [] location = return_address.location timestamps << { happened_at: printed_at.in_time_zone("America/New_York"), source: "Hack Club", facility: "Mailer", description: "Letter printed.", location:, } if printed_at timestamps << { happened_at: mailed_at.in_time_zone("America/New_York"), source: "Hack Club", facility: "Mailer", description: "Letter mailed!", location:, } if mailed_at timestamps << { happened_at: received_at.in_time_zone("America/New_York"), source: "You!", facility: "Your mailbox", description: "You received this letter!", location: "wherever you live", } if received_at (iv + timestamps).sort_by { |event| event[:happened_at] } end private def set_postage self.postage = case postage_type when "indicia" if usps_indicium.present? # Use actual indicia price if indicia are bought usps_indicium.cost elsif address.us? # For US mail without bought indicia, use metered price USPS::PricingEngine.metered_price( processing_category, weight, non_machinable ) else # For international mail without bought indicia, use FLIRT-ed price flirted = flirt USPS::PricingEngine.metered_price( flirted[:processing_category], flirted[:weight], flirted[:non_machinable] ) end when "stamps" if %i(queued).include?(aasm.current_state) return 0 end # For stamps, use stamp price for US and desired price for international if address.us? USPS::PricingEngine.domestic_stamp_price( processing_category, weight, non_machinable ) else USPS::PricingEngine.fcmi_price( processing_category, weight, address.country, non_machinable ) end when "international_origin" 0 end end def set_imb_sequence sn, rollover = usps_mailer_id.next_sn_and_rollover update_columns( imb_serial_number: sn, imb_rollover_count: rollover, ) end end