Module: Portage::Ucp::Etsy::Mapper

Defined in:
lib/portage/ucp/etsy/mapper.rb

Overview

Converts Etsy Open API v3 response bodies into the protocol-layer value objects from Portage::Ucp::ValueObjects — nothing Etsy-shaped is allowed to leak past this file.

Etsy's Money type ({amount, divisor, currency_code}) is already an integer, unlike every other adapter in this project — amount / divisor gives the decimal value, but amount itself is already in the currency's minor units when divisor is 100 (the only value Etsy documents ever returning), so no BigDecimal conversion is needed here at all.

Class Method Summary collapse

Class Method Details

.checkout(listings, id:, status:) ⇒ Object

id: is caller-supplied: there's no real Etsy checkout resource behind this at all — see Portage::Ucp::Etsy::Adapter's class-level comment. links points at each listing's own public Etsy URL rather than a cart, since Etsy's public API has no way to deep- link a multi-item add-to-cart flow.



82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/portage/ucp/etsy/mapper.rb', line 82

def checkout(listings, id:, status:)
  line_items = listings.map { |l| checkout_line_item(l) }
  currency = listings.first&.dig("price", "currency_code")
  Portage::Ucp::Checkout.new(
    id: id,
    status: status,
    line_items: line_items,
    currency: currency,
    totals: totals(listings),
    links: listings.map { |l| Portage::Ucp::Link.new(type: "checkout", url: l["url"], title: l["title"]) }
  )
end

.checkout_line_item(node) ⇒ Object



95
96
97
98
99
100
101
102
103
# File 'lib/portage/ucp/etsy/mapper.rb', line 95

def checkout_line_item(node)
  Portage::Ucp::LineItem.new(
    id: node["listing_id"].to_s,
    item: Portage::Ucp::Item.new(id: node["listing_id"].to_s, title: node["title"],
                                 price: node.dig("price", "amount")),
    quantity: node["quantity"] || 1,
    totals: Portage::Ucp::Support::Totals.line(line_total(node))
  )
end

.description(node) ⇒ Object



21
22
23
# File 'lib/portage/ucp/etsy/mapper.rb', line 21

def description(node)
  Portage::Ucp::Description.new(plain: node["description"])
end

.implicit_variant(node) ⇒ Object



61
62
63
64
65
# File 'lib/portage/ucp/etsy/mapper.rb', line 61

def implicit_variant(node)
  available = node["state"] == "active" && node["quantity"].to_i.positive?
  Portage::Ucp::Variant.new(id: node["listing_id"].to_s, title: node["title"], description: description(node),
                            price: price(node["price"]), availability: { "available" => available })
end

.line_total(node) ⇒ Object



105
106
107
# File 'lib/portage/ucp/etsy/mapper.rb', line 105

def line_total(node)
  node.dig("price", "amount") * (node["quantity"] || 1)
end

.money(node) ⇒ Object



17
18
19
# File 'lib/portage/ucp/etsy/mapper.rb', line 17

def money(node)
  Portage::Ucp::Money.new(amount_minor: node["amount"], currency: node["currency_code"])
end

.order(node) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/portage/ucp/etsy/mapper.rb', line 134

def order(node)
  status = receipt_status(node)
  Portage::Ucp::Order.new(
    id: node["receipt_id"].to_s,
    checkout_id: "",
    permalink_url: "",
    line_items: (node["transactions"] || []).map { |n| order_line_item(n, status) },
    fulfillment: Portage::Ucp::Fulfillment.new,
    currency: node.dig("total_price", "currency_code"),
    totals: Portage::Ucp::Support::Totals.summary(subtotal: money(node["subtotal"]).amount_minor,
                                                  total: money(node["total_price"]).amount_minor)
  )
end

.order_line_item(node, status) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/portage/ucp/etsy/mapper.rb', line 148

def order_line_item(node, status)
  quantity = node["quantity"]
  fulfilled = Portage::Ucp::Support::LineItemStatus.fulfilled_quantity(status, quantity)
  line_total = node.dig("price", "amount") * quantity
  Portage::Ucp::OrderLineItem.new(
    id: node["transaction_id"].to_s,
    item: Portage::Ucp::Item.new(id: node["listing_id"].to_s, title: node["title"],
                                 price: node.dig("price", "amount")),
    quantity: { original: quantity, total: quantity, fulfilled: fulfilled },
    totals: Portage::Ucp::Support::Totals.line(line_total),
    status: status
  )
end

.price(node) ⇒ Object



25
26
27
# File 'lib/portage/ucp/etsy/mapper.rb', line 25

def price(node)
  Portage::Ucp::Price.new(amount: node["amount"], currency: node["currency_code"])
end

.price_range(node) ⇒ Object



29
30
31
32
# File 'lib/portage/ucp/etsy/mapper.rb', line 29

def price_range(node)
  p = price(node)
  Portage::Ucp::PriceRange.new(min: p, max: p)
end

.product(node) ⇒ Object

node["variants_detail"] is adapter-populated, not a real Etsy field: a listing's own resource has no variant/SKU breakdown at all — that's a second call to the separate Inventory endpoint (/listings/{id}/inventory), made only for #get_product's single-listing path, same reasoning as every other adapter's N+1 variant fetch.



40
41
42
43
44
45
46
47
48
49
# File 'lib/portage/ucp/etsy/mapper.rb', line 40

def product(node)
  Portage::Ucp::Product.new(
    id: node["listing_id"].to_s,
    title: node["title"],
    description: description(node),
    price_range: price_range(node["price"]),
    variants: variants(node),
    url: node["url"]
  )
end

.receipt_status(node) ⇒ Object

Etsy core has no per-transaction fulfillment tracking on the Receipt resource beyond a single receipt-wide is_shipped flag — every line gets the same coarse status, same simplification as Portage::Ucp::WooCommerce::Mapper/Portage::Ucp::Magento::Mapper.

permalink_url is left blank — Etsy's API doesn't return a public, shareable receipt URL (buyers see receipts inside their own account, not via a link). checkout_id is always blank too, for a more fundamental reason than the other adapters: this gem's Checkout is never a real Etsy resource (see Portage::Ucp::Etsy::Adapter), so there is no "checkout that led to this receipt" for the adapter to have recorded in the first place.



127
128
129
130
131
132
# File 'lib/portage/ucp/etsy/mapper.rb', line 127

def receipt_status(node)
  return "fulfilled" if node["is_shipped"]
  return "removed" if node["status"] == "canceled"

  "processing"
end

.totals(listings) ⇒ Object



109
110
111
112
# File 'lib/portage/ucp/etsy/mapper.rb', line 109

def totals(listings)
  subtotal = listings.sum { |l| line_total(l) }
  Portage::Ucp::Support::Totals.summary(subtotal: subtotal, total: subtotal)
end

.variant(node, parent_node) ⇒ Object



67
68
69
70
71
72
73
74
75
# File 'lib/portage/ucp/etsy/mapper.rb', line 67

def variant(node, parent_node)
  title = (node["property_values"] || []).flat_map { |p| p["values"] }.join(" / ")
  offering = (node["offerings"] || []).find { |o| o["is_enabled"] } || {}
  Portage::Ucp::Variant.new(
    id: node["product_id"].to_s, title: title, description: description(parent_node),
    price: price(offering["price"] || {}),
    availability: { "available" => offering["quantity"].to_i.positive? }
  )
end

.variants(node) ⇒ Object

A listing with no inventory-level variation is its own single implicit variant, same as a single-variant Shopify product using that variant's own id.



54
55
56
57
58
59
# File 'lib/portage/ucp/etsy/mapper.rb', line 54

def variants(node)
  detail = node["variants_detail"]
  return [implicit_variant(node)] unless detail

  (detail["products"] || []).reject { |p| p["is_deleted"] }.map { |p| variant(p, node) }
end