Class: Portage::Ucp::ReferenceAdapter
Overview
In-memory Adapter implementing every capability in the contract,
including dev.ucp.shopping.discount/fulfillment/identity — roadmap §8
step 1 called for one of these ("no real backend required to prove the
protocol layer works") and it never shipped; spec/support/fake_adapter.rb
filled that gap for the core gem's own specs, but stayed test-only,
catalog/cart/checkout/order only, and undocumented outside this repo.
Two jobs: a copy-paste starting point for a third-party adapter author
(see README "Writing your own adapter"), and the fixture the
conformance kit (Portage::Ucp::RSpec, lib/portage/ucp/rspec.rb) runs its
own shared examples against to prove the kit itself is correct.
Not a mock — every mutating action does the real bookkeeping (line-item
totals, checkout status transitions, order adjustments) an adapter over
a live platform would, just against an in-process Hash instead of an
HTTP API. seed_product is the one method with no Adapter contract
counterpart — there is no real backend to seed real data into.
Constant Summary
collapse
- OUT_OF_STOCK_PREFIX =
Products whose id starts with this prefix are treated as sold out —
#complete_checkout raises Portage::Ucp::OutOfStockError for them, the
same way a real adapter's stock re-check (Adapter#complete_checkout's
docs, design-log §16) would. Lets the conformance kit exercise that
path without depending on adapter-specific seed data.
"oos_".freeze
Support::Idempotency::INIT_MUTEX
Instance Method Summary
collapse
-
#cancel_cart(cart_id:, idempotency_key:) ⇒ Object
-
#cancel_checkout(checkout_id:, idempotency_key:) ⇒ Object
-
#cancel_order(order_id:, idempotency_key:, reason: nil) ⇒ Object
-
#complete_checkout(checkout_id:, payment_token:, idempotency_key:) ⇒ Object
payment_token: is part of the Adapter contract's call signature (and already validated as non-PAN by PaymentTokenGuard before this ever runs, per §9) but this in-memory adapter has no payment processor to pass it on to — nothing here should hold onto it any longer than the single call needs to, so it's accepted and left unused rather than stored (see .rubocop.yml's Lint/UnusedMethodArgument exclude, same posture as the abstract Adapter#complete_checkout it overrides).
-
#create_cart(line_items:, idempotency_key:, discount_codes: nil) ⇒ Object
-
#create_checkout(line_items:, idempotency_key:, discount_codes: nil, fulfillment: nil) ⇒ Object
-
#discount_codes_supported? ⇒ Boolean
-
#fulfillment_supported? ⇒ Boolean
-
#get_cart(cart_id:) ⇒ Object
-
#get_checkout(checkout_id:) ⇒ Object
-
#get_order(order_id:) ⇒ Object
-
#get_product(product_id:) ⇒ Object
-
#initialize ⇒ ReferenceAdapter
constructor
A new instance of ReferenceAdapter.
-
#link_identity(oauth_token:) ⇒ Object
Accepts any non-blank token and mints a stable identity for it — real OAuth verification is a platform concern this in-memory adapter has no platform to defer to, same posture as PaymentTokenGuard drawing the line at "rejects the clearest misuse" rather than proving validity.
-
#refund_order(order_id:, line_items:, idempotency_key:, reason: nil) ⇒ Object
-
#reorder(order_id:, idempotency_key:) ⇒ Object
Not memoized against @carts' own id sequence namespace collision: next_id("cart") is shared with create_cart/update_cart, same as every other *_id prefix here.
-
#request_return(order_id:, line_items:, idempotency_key:, reason: nil) ⇒ Object
-
#search_catalog(query:, limit:) ⇒ Object
-
#seed_product(product) ⇒ Object
Not part of the Adapter contract — this adapter has no backend to seed real data into, so callers (a spec, a README example) hand it catalog fixtures directly.
-
#update_cart(cart_id:, line_items:, idempotency_key:, discount_codes: nil) ⇒ Object
-
#update_checkout(checkout_id:, line_items:, idempotency_key:, discount_codes: nil, fulfillment: nil) ⇒ Object
observability_key, with_observability
Constructor Details
Returns a new instance of ReferenceAdapter.
33
34
35
36
37
38
39
40
41
|
# File 'lib/portage/ucp/reference_adapter.rb', line 33
def initialize
super
@products = {}
@carts = {}
@checkouts = {}
@orders = {}
@identities = {}
@next_id = 0
end
|
Instance Method Details
#cancel_cart(cart_id:, idempotency_key:) ⇒ Object
75
76
77
78
79
80
|
# File 'lib/portage/ucp/reference_adapter.rb', line 75
def cancel_cart(cart_id:, idempotency_key:)
dedup(idempotency_key) do
@carts.delete(cart_id)
Portage::Ucp::Cart.new(id: cart_id, line_items: [], currency: "USD", totals: zero_totals)
end
end
|
#cancel_checkout(checkout_id:, idempotency_key:) ⇒ Object
120
121
122
123
124
125
126
|
# File 'lib/portage/ucp/reference_adapter.rb', line 120
def cancel_checkout(checkout_id:, idempotency_key:)
dedup(idempotency_key) do
record_checkout_status(checkout_id, "canceled")
checkout = @checkouts.fetch(checkout_id)
@checkouts[checkout_id] = Portage::Ucp::Checkout.new(**checkout.to_h, status: "canceled")
end
end
|
#cancel_order(order_id:, idempotency_key:, reason: nil) ⇒ Object
130
131
132
133
134
|
# File 'lib/portage/ucp/reference_adapter.rb', line 130
def cancel_order(order_id:, idempotency_key:, reason: nil)
dedup(idempotency_key) do
add_adjustment(order_id, type: "cancellation", status: "completed", description: reason)
end
end
|
#complete_checkout(checkout_id:, payment_token:, idempotency_key:) ⇒ Object
payment_token: is part of the Adapter contract's call signature (and
already validated as non-PAN by PaymentTokenGuard before this ever
runs, per §9) but this in-memory adapter has no payment processor to
pass it on to — nothing here should hold onto it any longer than the
single call needs to, so it's accepted and left unused rather than
stored (see .rubocop.yml's Lint/UnusedMethodArgument exclude, same
posture as the abstract Adapter#complete_checkout it overrides).
107
108
109
110
111
112
113
114
115
116
117
118
|
# File 'lib/portage/ucp/reference_adapter.rb', line 107
def complete_checkout(checkout_id:, payment_token:, idempotency_key:)
dedup(idempotency_key) do
checkout = @checkouts.fetch(checkout_id)
raise_if_any_line_out_of_stock!(checkout)
order = store_order(checkout)
confirmation = Portage::Ucp::OrderConfirmation.new(id: order.id, permalink_url: order.permalink_url)
record_checkout_status(checkout_id, "completed")
@checkouts[checkout_id] = Portage::Ucp::Checkout.new(**checkout.to_h, status: "completed",
order: confirmation)
end
end
|
#create_cart(line_items:, idempotency_key:, discount_codes: nil) ⇒ Object
62
63
64
65
66
67
|
# File 'lib/portage/ucp/reference_adapter.rb', line 62
def create_cart(line_items:, idempotency_key:, discount_codes: nil)
dedup(idempotency_key) do
id = next_id("cart")
@carts[id] = build_cart(id, line_items, discount_codes)
end
end
|
#create_checkout(line_items:, idempotency_key:, discount_codes: nil, fulfillment: nil) ⇒ Object
82
83
84
85
86
87
88
|
# File 'lib/portage/ucp/reference_adapter.rb', line 82
def create_checkout(line_items:, idempotency_key:, discount_codes: nil, fulfillment: nil)
dedup(idempotency_key) do
id = next_id("chk")
record_checkout_status(id, "incomplete")
@checkouts[id] = build_checkout(id, line_items, discount_codes, fulfillment, status: "incomplete")
end
end
|
#discount_codes_supported? ⇒ Boolean
163
|
# File 'lib/portage/ucp/reference_adapter.rb', line 163
def discount_codes_supported? = true
|
#fulfillment_supported? ⇒ Boolean
164
|
# File 'lib/portage/ucp/reference_adapter.rb', line 164
def fulfillment_supported? = true
|
#get_cart(cart_id:) ⇒ Object
60
|
# File 'lib/portage/ucp/reference_adapter.rb', line 60
def get_cart(cart_id:) = @carts[cart_id]
|
#get_checkout(checkout_id:) ⇒ Object
90
|
# File 'lib/portage/ucp/reference_adapter.rb', line 90
def get_checkout(checkout_id:) = @checkouts[checkout_id]
|
#get_order(order_id:) ⇒ Object
128
|
# File 'lib/portage/ucp/reference_adapter.rb', line 128
def get_order(order_id:) = @orders[order_id]
|
#get_product(product_id:) ⇒ Object
55
56
57
58
|
# File 'lib/portage/ucp/reference_adapter.rb', line 55
def get_product(product_id:)
product = @products[product_id]
product && Portage::Ucp::ProductDetail.new(product: product)
end
|
#link_identity(oauth_token:) ⇒ Object
Accepts any non-blank token and mints a stable identity for it — real
OAuth verification is a platform concern this in-memory adapter has
no platform to defer to, same posture as PaymentTokenGuard drawing
the line at "rejects the clearest misuse" rather than proving
validity.
171
172
173
174
175
176
177
178
|
# File 'lib/portage/ucp/reference_adapter.rb', line 171
def link_identity(oauth_token:)
raise Portage::Ucp::AuthenticationError, "blank oauth_token" if oauth_token.to_s.empty?
@identities[oauth_token] ||= Portage::Ucp::Identity.new(
subject: "user_#{Digest::SHA256.hexdigest(oauth_token)[0, 12]}",
email: nil, linked_at: Time.now.utc.iso8601
)
end
|
#refund_order(order_id:, line_items:, idempotency_key:, reason: nil) ⇒ Object
142
143
144
145
146
|
# File 'lib/portage/ucp/reference_adapter.rb', line 142
def refund_order(order_id:, line_items:, idempotency_key:, reason: nil)
dedup(idempotency_key) do
add_adjustment(order_id, type: "refund", status: "completed", line_items: line_items, description: reason)
end
end
|
#reorder(order_id:, idempotency_key:) ⇒ Object
Not memoized against @carts' own id sequence namespace collision:
next_id("cart") is shared with create_cart/update_cart, same as every
other *_id prefix here.
151
152
153
154
155
156
157
158
159
160
161
|
# File 'lib/portage/ucp/reference_adapter.rb', line 151
def reorder(order_id:, idempotency_key:)
dedup(idempotency_key) do
order = @orders[order_id]
next nil unless order
available, unavailable = partition_reorderable(order.line_items)
cart = build_cart(next_id("cart"), available, nil)
@carts[cart.id] = cart
Portage::Ucp::ReorderResult.new(cart: cart, unavailable_items: unavailable)
end
end
|
#request_return(order_id:, line_items:, idempotency_key:, reason: nil) ⇒ Object
136
137
138
139
140
|
# File 'lib/portage/ucp/reference_adapter.rb', line 136
def request_return(order_id:, line_items:, idempotency_key:, reason: nil)
dedup(idempotency_key) do
add_adjustment(order_id, type: "return", status: "pending", line_items: line_items, description: reason)
end
end
|
#search_catalog(query:, limit:) ⇒ Object
50
51
52
53
|
# File 'lib/portage/ucp/reference_adapter.rb', line 50
def search_catalog(query:, limit:)
matches = @products.values.select { |p| p.title.downcase.include?(query.downcase) }.first(limit)
Portage::Ucp::CatalogSearchResult.new(products: matches)
end
|
#seed_product(product) ⇒ Object
Not part of the Adapter contract — this adapter has no backend to
seed real data into, so callers (a spec, a README example) hand it
catalog fixtures directly.
46
47
48
|
# File 'lib/portage/ucp/reference_adapter.rb', line 46
def seed_product(product)
@products[product.id] = product
end
|
#update_cart(cart_id:, line_items:, idempotency_key:, discount_codes: nil) ⇒ Object
69
70
71
72
73
|
# File 'lib/portage/ucp/reference_adapter.rb', line 69
def update_cart(cart_id:, line_items:, idempotency_key:, discount_codes: nil)
dedup(idempotency_key) do
@carts[cart_id] = build_cart(cart_id, line_items, discount_codes, previous: @carts[cart_id])
end
end
|
#update_checkout(checkout_id:, line_items:, idempotency_key:, discount_codes: nil, fulfillment: nil) ⇒ Object
92
93
94
95
96
97
98
|
# File 'lib/portage/ucp/reference_adapter.rb', line 92
def update_checkout(checkout_id:, line_items:, idempotency_key:, discount_codes: nil, fulfillment: nil)
dedup(idempotency_key) do
record_checkout_status(checkout_id, "incomplete")
@checkouts[checkout_id] = build_checkout(checkout_id, line_items, discount_codes, fulfillment,
status: "incomplete", previous: @checkouts[checkout_id])
end
end
|