-
Notifications
You must be signed in to change notification settings - Fork 57
feat: inkthreadable #701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: inkthreadable #701
Changes from all commits
d214144
1fd70d5
514cd29
5b7f30b
420af97
bd6ae76
4956398
f1551f2
93dc8ca
99cf6f3
31ad2ab
6fc39e0
f8658c3
2dd5495
b5eb708
18e5a23
e8fbad9
48b126e
d540895
ead3d04
23e17ff
55dc09b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
| 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, | ||
NeonGamerBot-QK marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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? | ||
NeonGamerBot-QK marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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
|
||
| 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 | ||
|
||
| ALERT_SLACK_ID = "U054VC2KM9P" # @transcental | ||
NeonGamerBot-QK marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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) | ||
NeonGamerBot-QK marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
NeonGamerBot-QK marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
| 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 |
There was a problem hiding this comment.
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.