Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d214144
update: wip inkthreadable items
NeonGamerBot-QK Dec 29, 2025
1fd70d5
update: add item to new shop item
NeonGamerBot-QK Dec 30, 2025
514cd29
update: add job to sync status
NeonGamerBot-QK Jan 16, 2026
5b7f30b
Merge branch 'main' into inkthreadable
NeonGamerBot-QK Jan 16, 2026
420af97
Merge branch 'main' into inkthreadable
transcental Jan 16, 2026
bd6ae76
Merge branch 'main' into inkthreadable
NeonGamerBot-QK Jan 18, 2026
4956398
update: annotate + req changes
NeonGamerBot-QK Jan 21, 2026
f1551f2
this is amber
NeonGamerBot-QK Jan 21, 2026
93dc8ca
fix: ambers req changes
NeonGamerBot-QK Jan 21, 2026
99cf6f3
Merge remote-tracking branch 'refs/remotes/origin/inkthreadable' into…
NeonGamerBot-QK Jan 21, 2026
31ad2ab
Update app/controllers/admin/shop_items_controller.rb
NeonGamerBot-QK Jan 21, 2026
6fc39e0
fix: i dislike the docs
NeonGamerBot-QK Jan 21, 2026
f8658c3
Merge remote-tracking branch 'refs/remotes/origin/inkthreadable' into…
NeonGamerBot-QK Jan 21, 2026
2dd5495
Update app/models/shop_item/inkthreadable_item.rb
NeonGamerBot-QK Jan 21, 2026
b5eb708
fix: db stuff + lint + everythig
NeonGamerBot-QK Jan 21, 2026
18e5a23
Merge branch 'main' into inkthreadable
NeonGamerBot-QK Jan 23, 2026
e8fbad9
Merge branch 'main' into inkthreadable
NeonGamerBot-QK Jan 27, 2026
48b126e
Merge branch 'main' into inkthreadable
NeonGamerBot-QK Jan 30, 2026
d540895
Potential fix for code scanning alert no. 19: Use of a broken or weak…
NeonGamerBot-QK Feb 8, 2026
ead3d04
Merge branch 'main' into inkthreadable
NeonGamerBot-QK Feb 9, 2026
23e17ff
update: schema
NeonGamerBot-QK Feb 9, 2026
55dc09b
Merge branch 'main' into inkthreadable
transcental Feb 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion app/controllers/admin/shop_items_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def available_shop_item_types
"ShopItem::HCBGrant",
"ShopItem::HCBPreauthGrant",
"ShopItem::HQMailItem",
"ShopItem::InkthreadableItem",
"ShopItem::LetterMail",
"ShopItem::ThirdPartyPhysical",
"ShopItem::ThirdPartyDigital",
Expand All @@ -108,7 +109,7 @@ def available_shop_item_types
end

def shop_item_params
params.require(:shop_item).permit(
permitted = params.require(:shop_item).permit(
:name,
:type,
:description,
Expand Down Expand Up @@ -164,11 +165,23 @@ def shop_item_params
:default_assigned_user_id_au,
:default_assigned_user_id_in,
:default_assigned_user_id_xx,
:inkthreadable_config,
:unlisted,
:source_region,
attached_shop_item_ids: [],
blocked_countries: []
)

if permitted[:inkthreadable_config].present?
begin
permitted[:inkthreadable_config] = JSON.parse(permitted[:inkthreadable_config])
rescue JSON::ParserError
flash.now[:alert] = "Inkthreadable config must be valid JSON."
permitted[:inkthreadable_config] = nil
end
end

permitted
end
end
end
77 changes: 77 additions & 0 deletions app/jobs/shop/send_inkthreadable_order_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

class Shop::SendInkthreadableOrderJob < ApplicationJob
queue_as :default

def perform(order_id)
order = ShopOrder.find(order_id)
shop_item = order.shop_item

unless shop_item.is_a?(ShopItem::InkthreadableItem)
Rails.logger.warn "[Inkthreadable] Order #{order_id} is not an Inkthreadable item, skipping"
return
end

address = order.frozen_address
if address.blank?
Rails.logger.error "[Inkthreadable] Order #{order_id} missing address"
return
end

payload = build_order_payload(order, shop_item, address)

response = InkthreadableService.create_order(payload)

inkthreadable_order_id = response.dig("order", "id")
if inkthreadable_order_id.present?
order.update!(external_ref: "INK-#{inkthreadable_order_id}")
Rails.logger.info "[Inkthreadable] Order #{order_id} submitted successfully as #{inkthreadable_order_id}"
else
Rails.logger.error "[Inkthreadable] Order #{order_id} response missing order.id: #{response.inspect}"
raise "Inkthreadable response missing order.id"
end
rescue Faraday::Error => e
Rails.logger.error "[Inkthreadable] Failed to send order #{order_id}: #{e.message}"
Rails.logger.error e.response&.dig(:body) if e.respond_to?(:response)
raise
end

private

def build_order_payload(order, shop_item, address)
payload = {
external_id: "FT-#{order.id}",
shipping_address: {
firstName: address["first_name"] || address["firstName"] || order.user.display_name.split.first,
lastName: address["last_name"] || address["lastName"] || order.user.display_name.split.last,
Comment on lines +42 to +46
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lastName fallback uses display_name.split.last which will return the entire display_name if there are no spaces. For single-word display names, this could result in the same value being used for both firstName and lastName. Consider handling this edge case more gracefully.

Suggested change
payload = {
external_id: "FT-#{order.id}",
shipping_address: {
firstName: address["first_name"] || address["firstName"] || order.user.display_name.split.first,
lastName: address["last_name"] || address["lastName"] || order.user.display_name.split.last,
user_display_name = order.user&.display_name.to_s
name_parts = user_display_name.strip.split(/\s+/)
fallback_first_name = name_parts.first
fallback_last_name = name_parts.length > 1 ? name_parts.last : nil
payload = {
external_id: "FT-#{order.id}",
shipping_address: {
firstName: address["first_name"] || address["firstName"] || fallback_first_name,
lastName: address["last_name"] || address["lastName"] || fallback_last_name,

Copilot uses AI. Check for mistakes.
company: address["company"],
address1: address["address1"] || address["line1"],
address2: address["address2"] || address["line2"],
city: address["city"],
county: address["state"] || address["county"],
postcode: address["postcode"] || address["zip"] || address["postal_code"],
country: address["country"],
phone1: address["phone"] || address["phone1"]
}.compact,
shipping: {
shippingMethod: shop_item.shipping_method
},
items: build_order_items(order, shop_item)
}

payload[:brandName] = shop_item.brand_name if shop_item.brand_name.present?
payload[:comment] = "Flavortown order ##{order.id}" if Rails.env.production?

payload
end

def build_order_items(order, shop_item)
[
{
pn: shop_item.product_number,
quantity: order.quantity,
designs: shop_item.design_urls
}.compact
]
end
end
Comment on lines +1 to +77
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Shop::SendInkthreadableOrderJob lacks test coverage. Consider adding tests to verify payload construction with various address formats, error handling when addresses are missing required fields, and proper handling of Faraday errors.

Copilot uses AI. Check for mistakes.
66 changes: 66 additions & 0 deletions app/jobs/shop/sync_inkthreadable_status_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
class Shop::SyncInkthreadableStatusJob < ApplicationJob
queue_as :default

SHIPPED_STATUSES = [ "quality control" ].freeze
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant SHIPPED_STATUSES is defined but never used in the code. This appears to be dead code that should either be removed or the logic should be updated to use it.

Copilot uses AI. Check for mistakes.
ALERT_SLACK_ID = "U054VC2KM9P" # @transcental

def perform
pending_inkthreadable_orders.find_each do |order|
sync_order_status(order)
rescue => e
Rails.logger.error "[InkthreadableSync] Failed to sync order #{order.id}: #{e.message}"
end
end

private

def pending_inkthreadable_orders
ShopOrder
.joins(:shop_item)
.where(shop_items: { type: "ShopItem::InkthreadableItem" })
.where.not(aasm_state: %w[fulfilled rejected refunded])
.where("external_ref LIKE 'INK-%'")
end

def sync_order_status(order)
inkthreadable_id = order.external_ref.delete_prefix("INK-")
response = InkthreadableService.get_order(inkthreadable_id)

ink_order = response["order"]
return unless ink_order

status = ink_order["status"]&.downcase
shipping = ink_order["shipping"] || {}
tracking_number = shipping["trackingNumber"]
# either the docs are wrong or its the brits faults - neon
shipped_at = shipping["shiped_at"] || shipping["shipped_at"]

if shipped_at.present? || tracking_number.present?
mark_as_fulfilled(order, tracking_number)
elsif status == "refunded" || ink_order["deleted"] == "true"
handle_cancelled(order)
else
handle_unexpected_status(order, status)
end
end

def handle_unexpected_status(order, status)
message = "[InkthreadableSync] Order #{order.id} has unexpected status: #{status}"
Rails.logger.warn message

Sentry.capture_message(message, level: :warning, extra: { order_id: order.id, status: status })

order.send_fulfillment_alert!("Inkthreadable order has unexpected status: *#{status}*")
end

def mark_as_fulfilled(order, tracking_number)
external_ref = tracking_number.present? ? "INK-#{tracking_number}" : order.external_ref
order.mark_fulfilled!(external_ref)
Rails.logger.info "[InkthreadableSync] Order #{order.id} marked as fulfilled with tracking: #{tracking_number}"
end

def handle_cancelled(order)
Rails.logger.warn "[InkthreadableSync] Order #{order.id} was refunded/cancelled in Inkthreadable"
order.mark_rejected!("Cancelled by Inkthreadable")
end
end
1 change: 1 addition & 0 deletions app/models/shop_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
1 change: 1 addition & 0 deletions app/models/shop_item/accessory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
1 change: 1 addition & 0 deletions app/models/shop_item/free_stickers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
1 change: 1 addition & 0 deletions app/models/shop_item/h_c_b_grant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
1 change: 1 addition & 0 deletions app/models/shop_item/h_c_b_preauth_grant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
1 change: 1 addition & 0 deletions app/models/shop_item/h_q_mail_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
1 change: 1 addition & 0 deletions app/models/shop_item/hack_clubber_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
1 change: 1 addition & 0 deletions app/models/shop_item/inkthreadable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
104 changes: 104 additions & 0 deletions app/models/shop_item/inkthreadable_item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# == Schema Information
#
# Table name: shop_items
#
# id :bigint not null, primary key
# accessory_tag :string
# agh_contents :jsonb
# attached_shop_item_ids :bigint default([]), is an Array
# buyable_by_self :boolean default(TRUE)
# default_assigned_user_id_au :bigint
# default_assigned_user_id_ca :bigint
# default_assigned_user_id_eu :bigint
# default_assigned_user_id_in :bigint
# default_assigned_user_id_uk :bigint
# default_assigned_user_id_us :bigint
# default_assigned_user_id_xx :bigint
# description :string
# enabled :boolean
# enabled_au :boolean
# enabled_ca :boolean
# enabled_eu :boolean
# enabled_in :boolean
# enabled_uk :boolean
# enabled_us :boolean
# enabled_xx :boolean
# hacker_score :integer
# hcb_category_lock :string
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
# max_qty :integer
# name :string
# old_prices :integer default([]), is an Array
# one_per_person_ever :boolean
# past_purchases :integer default(0)
# payout_percentage :integer default(0)
# price_offset_au :decimal(, )
# price_offset_ca :decimal(, )
# price_offset_eu :decimal(, )
# price_offset_in :decimal(, )
# price_offset_uk :decimal(10, 2)
# price_offset_us :decimal(, )
# price_offset_xx :decimal(, )
# required_ships_count :integer default(1)
# required_ships_end_date :date
# required_ships_start_date :date
# requires_achievement :string
# requires_ship :boolean default(FALSE)
# sale_percentage :integer
# show_in_carousel :boolean
# site_action :integer
# source_region :string
# special :boolean
# stock :integer
# ticket_cost :decimal(, )
# type :string
# unlisted :boolean default(FALSE)
# unlock_on :date
# usd_cost :decimal(, )
# created_at :datetime not null
# updated_at :datetime not null
# default_assigned_user_id :bigint
# user_id :bigint
#
# Indexes
#
# index_shop_items_on_default_assigned_user_id (default_assigned_user_id)
# index_shop_items_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (default_assigned_user_id => users.id) ON DELETE => nullify
# fk_rails_... (user_id => users.id)
#
class ShopItem::InkthreadableItem < ShopItem
def fulfill!(shop_order)
Shop::SendInkthreadableOrderJob.perform_later(shop_order.id)
shop_order.queue_for_fulfillment!
end

def inkthreadable_config
super || {}
end

def product_number
inkthreadable_config["pn"]
end

def design_urls
inkthreadable_config["designs"] || {}
end

def shipping_method
inkthreadable_config["shipping_method"] || "regular"
end

def brand_name
inkthreadable_config["brand_name"]
end
end
1 change: 1 addition & 0 deletions app/models/shop_item/letter_mail.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
1 change: 1 addition & 0 deletions app/models/shop_item/pile_of_stickers_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
1 change: 1 addition & 0 deletions app/models/shop_item/special_fulfillment_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
1 change: 1 addition & 0 deletions app/models/shop_item/third_party_digital.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
1 change: 1 addition & 0 deletions app/models/shop_item/third_party_physical.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
# hcb_keyword_lock :string
# hcb_merchant_lock :string
# hcb_preauthorization_instructions :text
# inkthreadable_config :jsonb
# internal_description :string
# limited :boolean
# long_description :text
Expand Down
Loading
Loading