mirror of
https://github.com/System-End/theseus.git
synced 2026-04-19 19:55:10 +00:00
302 lines
8.7 KiB
Ruby
302 lines
8.7 KiB
Ruby
# == 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
|