warehouse api!

This commit is contained in:
24c02 2025-06-05 17:21:56 -04:00
parent 0654801c52
commit 911c6787e0
13 changed files with 211 additions and 35 deletions

View file

@ -27,6 +27,10 @@ module API
render json: { error: "idempotency_error", messages: ["a record by that idempotency key already exists!"] }, status: :bad_request
end
rescue_from ActionController::ParameterMissing do |e|
render json: { error: "missing_parameter", messages: [e.message] }, status: :bad_request
end
private
def set_expand

View file

@ -1,6 +1,8 @@
module API
module V1
class LetterQueuesController < ApplicationController
include AddressParameterParsing
before_action :set_letter_queue, only: [:show, :create_letter]
before_action :set_instant_letter_queue, only: [:create_instant_letter, :queued]
@ -36,18 +38,8 @@ module API
def create_letter
authorize @letter_queue
# Normalize country name using FrickinCountryNames
country = FrickinCountryNames.find_country(letter_params[:address][:country])
if country.nil?
render json: { error: "couldn't figure out country name #{letter_params[:address][:country]}" }, status: :unprocessable_entity
return
end
# Create address with normalized country
address_params = letter_params[:address].merge(country: country.alpha2)
# Normalize state name to abbreviation
address_params[:state] = FrickinCountryNames.normalize_state(country, address_params[:state])
addy = Address.new(address_params)
addy = parse_address_from_params(letter_params[:address])
return unless addy
@letter = @letter_queue.create_letter!(
addy,
@ -62,18 +54,8 @@ module API
def create_instant_letter
authorize @letter_queue, policy_class: Letter::QueuePolicy
# Normalize country name using FrickinCountryNames
country = FrickinCountryNames.find_country(letter_params[:address][:country])
if country.nil?
render json: { error: "couldn't figure out country name #{letter_params[:address][:country]}" }, status: :unprocessable_entity
return
end
# Create address with normalized country
address_params = letter_params[:address].merge(country: country.alpha2)
# Normalize state name to abbreviation
address_params[:state] = FrickinCountryNames.normalize_state(country, address_params[:state])
addy = Address.new(address_params)
addy = parse_address_from_params(letter_params[:address])
return unless addy
@letter = @letter_queue.process_letter_instantly!(
addy,
@ -118,11 +100,6 @@ module API
],
)
end
def normalize_country(country)
return "US" if country.blank? || country.downcase == "usa" || country.downcase == "united states"
country
end
end
end
end

View file

@ -0,0 +1,111 @@
module API
module V1
class WarehouseOrdersController < ApplicationController
include AddressParameterParsing
before_action :set_warehouse_order, only: [:show]
rescue_from ActiveRecord::RecordNotFound do |e|
render json: { error: "Warehouse order not found" }, status: :not_found
end
rescue_from ActiveRecord::RecordInvalid do |e|
Honeybadger.notify(e)
render json: {
error: "Validation failed",
details: e.record.errors.full_messages,
}, status: :unprocessable_entity
end
def show
authorize @warehouse_order
end
def index
@warehouse_orders = policy_scope(Warehouse::Order)
end
def from_template
@template = Warehouse::Template.find_by_public_id!(params[:template_id])
address = parse_address_from_params(permit_address_params)
@warehouse_order = Warehouse::Order.from_template(@template, warehouse_order_params.merge(address:, user: current_user))
authorize @warehouse_order
# Build additional line items from contents if provided
if params[:contents].present?
contents_params.each do |content_item|
sku = Warehouse::SKU.find_by(sku: content_item[:sku])
unless sku
render json: { error: "SKU not found: #{content_item[:sku]}" }, status: :unprocessable_entity
return
end
@warehouse_order.line_items.build(
sku: sku,
quantity: content_item[:quantity],
)
end
end
address.save!
@warehouse_order.save!
@warehouse_order.dispatch!
render :show, status: :created
end
def create
address = parse_address_from_params(permit_address_params)
@warehouse_order = Warehouse::Order.new(warehouse_order_params.merge(address:, user: current_user, source_tag: SourceTag.first))
authorize @warehouse_order
address.save!
# Build line items from contents
contents_params.each do |content_item|
sku = Warehouse::SKU.find_by(sku: content_item[:sku])
unless sku
render json: { error: "SKU not found: #{content_item[:sku]}" }, status: :unprocessable_entity
return
end
@warehouse_order.line_items.build(
sku: sku,
quantity: content_item[:quantity],
)
end
@warehouse_order.save!
@warehouse_order.dispatch!
render :show, status: :created
end
private
def set_warehouse_order
@warehouse_order = policy_scope(Warehouse::Order).find_by!(hc_id: params[:id])
end
def warehouse_order_params
params.require(:warehouse_order).permit(
:recipient_email,
:user_facing_title,
:idempotency_key,
metadata: {},
tags: [],
).tap do |wp|
wp.require(:recipient_email)
wp.require(:tags)
raise ActionController::ParameterMissing.new(:tags) if wp[:tags].blank? || wp[:tags].empty?
end
end
def contents_params
return [] unless params[:contents].present?
params.expect(contents: [[:sku, :quantity]]).map.with_index do |content_item, index|
content_item.tap do |cp|
raise ActionController::ParameterMissing.new([:contents, index, :sku]) unless cp[:sku].present?
raise ActionController::ParameterMissing.new([:contents, index, :quantity]) unless cp[:quantity].present?
end
end
end
end
end
end

View file

@ -0,0 +1,38 @@
module AddressParameterParsing
extend ActiveSupport::Concern
private
def parse_address_from_params(address_params)
return nil if address_params.blank?
Address.new(address_params).validate!
# Normalize country name using FrickinCountryNames
country = FrickinCountryNames.find_country(address_params[:country])
if country.nil?
render json: { error: "couldn't figure out country name #{address_params[:country]}" }, status: :unprocessable_entity
return nil
end
# Create address with normalized country
normalized_address_params = address_params.merge(country: country.alpha2)
# Normalize state name to abbreviation
normalized_address_params[:state] = FrickinCountryNames.normalize_state(country, normalized_address_params[:state])
Address.new(normalized_address_params)
end
def permit_address_params
params.require(:address).permit(
:first_name,
:last_name,
:line_1,
:line_2,
:city,
:state,
:postal_code,
:country
)
end
end

View file

@ -22,6 +22,10 @@
#
class Warehouse::Template < ApplicationRecord
include HasWarehouseLineItems
include PublicIdentifiable
set_public_id_prefix "wot"
scope :shared, -> { where(public: true) }
belongs_to :user

View file

@ -7,6 +7,8 @@ class Warehouse::OrderPolicy < ApplicationPolicy
user_can_warehouse
end
alias_method :from_template?, :create?
def edit?
return false unless user_can_warehouse
record_belongs_to_user || user_is_admin

View file

@ -0,0 +1,18 @@
{
id: order.hc_id,
status: order.aasm_state,
tags: order.tags,
address: render(order.address),
metadata: order.metadata || {},
recipient_email: order.recipient_email,
dispatched_at: order.dispatched_at,
mailed_at: order.mailed_at,
tracking_number: order.tracking_number,
carrier: order.carrier,
service: order.service,
weight: order.weight,
contents_cost: order.contents_cost,
labor_cost: order.labor_cost,
postage_cost: order.postage_cost,
idempotency_key: order.idempotency_key,
}.compact_blank

View file

@ -0,0 +1,3 @@
{
warehouse_orders: @warehouse_orders.map { |o| render o },
}

View file

@ -0,0 +1,3 @@
{
warehouse_order: render(@warehouse_order),
}

View file

@ -9,10 +9,13 @@
<%= render 'shared/user_mention', user: template.user %>
</div>
</div>
<div class="mt-4">
<small class="text-muted">Contents:</small>
<%= render 'warehouse/orders/line_items', order: template %>
</div>
<div class="mt-4">
<small class="text-muted">API ID:</small>
<%= template.public_id %>
</div>
</div>
</article>

View file

@ -682,6 +682,11 @@ Rails.application.routes.draw do
get :letters
end
end
resources :warehouse_orders, only: [:show, :index, :create] do
collection do
post "from_template/:template_id", to: "warehouse_orders#from_template", as: :from_template
end
end
end
end
end

View file

@ -10,10 +10,10 @@
SourceTag.find_or_create_by!(
name: "Theseus web interface",
slug: "theseus_web",
owner: "Nora"
owner: "Nora",
)
Warehouse::PurposeCode.find_or_create_by!(
code: Rails.env.production? ? 'HQ' : 'HQ-dev',
description: 'general HQ mailing'
)
# Warehouse::PurposeCode.find_or_create_by!(
# code: Rails.env.production? ? 'HQ' : 'HQ-dev',
# description: 'general HQ mailing'
# )

View file

@ -0,0 +1,8 @@
# This rake task was added by annotate_rb gem.
# Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this
if Rails.env.development? && ENV["ANNOTATERB_SKIP_ON_DB_TASKS"].nil?
require "annotate_rb"
AnnotateRb::Core.load_rake_tasks
end