portage-ucp-woocommerce 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE +21 -0
- data/README.md +129 -0
- data/exe/portage-ucp-woocommerce +31 -0
- data/lib/portage/ucp/woocommerce/adapter.rb +193 -0
- data/lib/portage/ucp/woocommerce/client.rb +92 -0
- data/lib/portage/ucp/woocommerce/errors.rb +22 -0
- data/lib/portage/ucp/woocommerce/mapper.rb +170 -0
- data/lib/portage/ucp/woocommerce/version.rb +7 -0
- data/lib/portage/ucp/woocommerce.rb +6 -0
- metadata +129 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c4a4539d949226ab96ca758ccbea5d8ad40c789a7bd25d5512b9af0fcf8c0240
|
|
4
|
+
data.tar.gz: '0289fb79597b556193107a4d32efb3b10f2ae1800df73910e2d62793799fd6d8'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9b442c458f7157abb0140eabb5700d87fdb7f55d7c34d740b4057d5530873bc6200342a5017aea73d402d04c86f7111858d369de7578907ed622164c4f4e36e3
|
|
7
|
+
data.tar.gz: 5790802bc3de14b69490c41eb50902d2ba36889256cb7077c990da6b296aff6c103c1f10a661e63dfb5a0372805a2f09a81bc51ba9673ec5c91dfd1ec1ba879f
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. Format loosely follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/); this project is
|
|
5
|
+
pre-1.0, so APIs may still shift between minor versions.
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - Unreleased
|
|
8
|
+
|
|
9
|
+
- Initial pre-release. WooCommerce adapter against the Admin REST API v3
|
|
10
|
+
(catalog, order) and Store API v1 (cart, checkout).
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tom Whitbread
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# portage-ucp-woocommerce
|
|
2
|
+
|
|
3
|
+
WooCommerce adapter for [`portage-ucp`](../portage-ucp). Implements `Portage::Ucp::Adapter` against a WooCommerce site's Admin REST API v3 (catalog, order) and Store API v1 (cart, checkout). Generic only — no merchant-specific business logic. Plain `Net::HTTP`, no `woocommerce-api` runtime dependency.
|
|
4
|
+
|
|
5
|
+
## What it covers
|
|
6
|
+
|
|
7
|
+
| UCP capability | Backing WooCommerce API | Notes |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `dev.ucp.shopping.catalog` | Admin REST v3 | `search_catalog`, `get_product` |
|
|
10
|
+
| `dev.ucp.shopping.cart` | Store API (Cart) | `get_cart`, `create_cart`, `update_cart`, `cancel_cart` |
|
|
11
|
+
| `dev.ucp.shopping.checkout` | Store API (same Cart, plus `/checkout`) | `create_checkout`, `get_checkout`, `update_checkout`, `complete_checkout`, `cancel_checkout` |
|
|
12
|
+
| `dev.ucp.shopping.order` | Admin REST v3 | `get_order` |
|
|
13
|
+
| `dev.ucp.shopping.identity` | — | not implemented; WordPress/WooCommerce user auth is a separate concern from the Admin keys + anonymous Store API session used here |
|
|
14
|
+
|
|
15
|
+
Like Shopify, WooCommerce has no separate "Checkout" object — the Store API's Cart **is** the checkout, identified by an opaque `Cart-Token` session header rather than a resource id. The adapter tracks checkout status itself, keyed by that token, and records `Order#checkout_id` itself at completion time (WooCommerce orders don't link back to their originating cart natively).
|
|
16
|
+
|
|
17
|
+
Update/replace operations (`update_cart`, `update_checkout`) are full-replacement: the Store API has no atomic "replace all lines" mutation either, so the adapter removes every current line then re-adds the desired ones. Mutating methods dedup by `idempotency_key` in-process so a dropped-connection retry can't double-charge.
|
|
18
|
+
|
|
19
|
+
Unlike Shopify/Wix, there's no `AccessTokenFetcher` — WooCommerce Admin keys are static, generated once in wp-admin (WooCommerce → Settings → Advanced → REST API), with nothing to exchange or expire.
|
|
20
|
+
|
|
21
|
+
## ⚠️ Unverified against a live site
|
|
22
|
+
|
|
23
|
+
Built from WooCommerce's documented REST/Store API shapes, not run against a real site yet. Before relying on this in production, confirm:
|
|
24
|
+
|
|
25
|
+
- **`#complete_checkout`** — posts `payment_method` + `payment_data` to the Store API's `/checkout` endpoint. `payment_method` must match an installed, enabled WC gateway id, and the `payment_data` key a gateway expects (`payment_data_key:`, default `"token"`) is gateway-specific — Stripe's block-based gateway and PayPal's don't necessarily read the same key. Needs confirming against a live site with a real gateway installed.
|
|
26
|
+
- **Variable product variants** — `Mapper.variant`'s title is built by joining `attributes[].option`; not verified against a real variable product's actual attribute shape.
|
|
27
|
+
- **Order fulfillment** — WooCommerce core has no per-line-item fulfillment tracking (that's a shipment-tracking-plugin concern), so every order line is given the same coarse status derived from the order's own top-level `status`, not a real per-line signal.
|
|
28
|
+
- **Store API checkout response shape** — assumed to carry the same `items`/`totals` shape as the Cart response, plus `order_id`. Not confirmed live.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# Gemfile
|
|
34
|
+
gem "portage-ucp-woocommerce"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bundle install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Setup
|
|
42
|
+
|
|
43
|
+
You need a site URL, an Admin REST API consumer key/secret pair (wp-admin → WooCommerce → Settings → Advanced → REST API — grant Read/Write), your store's currency (the Admin product resource doesn't return one), and — only if you'll call `complete_checkout` — the WC payment gateway id you want to submit orders through.
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
require "portage/ucp/woocommerce"
|
|
47
|
+
|
|
48
|
+
client = Portage::Ucp::WooCommerce::Client.new(
|
|
49
|
+
site_url: "https://your-shop.example.com",
|
|
50
|
+
consumer_key: ENV.fetch("WOOCOMMERCE_CONSUMER_KEY"),
|
|
51
|
+
consumer_secret: ENV.fetch("WOOCOMMERCE_CONSUMER_SECRET")
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
adapter = Portage::Ucp::WooCommerce::Adapter.new(
|
|
55
|
+
client: client,
|
|
56
|
+
site_url: "https://your-shop.example.com",
|
|
57
|
+
currency: "USD",
|
|
58
|
+
payment_method: "stripe_cc" # only required for #complete_checkout
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# Catalog
|
|
66
|
+
products = adapter.search_catalog(query: "hoodie", limit: 10)
|
|
67
|
+
product = adapter.get_product(product_id: products.first.id)
|
|
68
|
+
|
|
69
|
+
# Cart — cart_id is only known after the first call, since the Store API
|
|
70
|
+
# assigns it (as a Cart-Token) rather than taking one from the caller
|
|
71
|
+
cart = adapter.create_cart(
|
|
72
|
+
line_items: [{ product_id: product.variants.first[:id], quantity: 2 }],
|
|
73
|
+
idempotency_key: SecureRandom.uuid
|
|
74
|
+
)
|
|
75
|
+
cart = adapter.update_cart(cart_id: cart.id, line_items: [], idempotency_key: SecureRandom.uuid) # empties cart
|
|
76
|
+
|
|
77
|
+
# Checkout
|
|
78
|
+
checkout = adapter.create_checkout(
|
|
79
|
+
line_items: [{ product_id: product.variants.first[:id], quantity: 1 }],
|
|
80
|
+
idempotency_key: SecureRandom.uuid
|
|
81
|
+
)
|
|
82
|
+
checkout = adapter.complete_checkout(
|
|
83
|
+
checkout_id: checkout.id,
|
|
84
|
+
payment_token: single_use_token_from_payment_handler,
|
|
85
|
+
idempotency_key: SecureRandom.uuid
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Order
|
|
89
|
+
order = adapter.get_order(order_id: checkout_order_id) # only once linked post-completion
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Wiring into portage-ucp
|
|
93
|
+
|
|
94
|
+
Drop the adapter into a `Dispatcher` (or the MCP server) the same as any other backend:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
dispatcher = Portage::Ucp::Dispatcher.new(adapter: adapter)
|
|
98
|
+
|
|
99
|
+
dispatcher.call(
|
|
100
|
+
capability: "dev.ucp.shopping.cart",
|
|
101
|
+
action: "create",
|
|
102
|
+
arguments: { line_items: [{ product_id: variant_id, quantity: 1 }], idempotency_key: SecureRandom.uuid }
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Because `link_identity` is left unoverridden, `Capability#advertised_for?` simply won't advertise `dev.ucp.shopping.identity` for this adapter — callers get an absent capability, not a 500.
|
|
107
|
+
|
|
108
|
+
## Errors
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
Portage::Ucp::WooCommerce::Error # base class
|
|
112
|
+
Portage::Ucp::WooCommerce::ApiError # any non-2xx response from either the Admin or Store API
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
bundle exec rspec # tests (WebMock-stubbed, no live site needed)
|
|
119
|
+
bundle exec rubocop # lint
|
|
120
|
+
|
|
121
|
+
# verify a real site's Admin key/secret by listing one product
|
|
122
|
+
WOOCOMMERCE_SITE_URL=https://your-shop.example.com \
|
|
123
|
+
WOOCOMMERCE_CONSUMER_KEY=... WOOCOMMERCE_CONSUMER_SECRET=... \
|
|
124
|
+
bundle exec rake woocommerce_smoke_test
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
[MIT](LICENSE) — Copyright (c) 2026 Tom Whitbread.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Standalone MCP server over stdio. Configure the site connection via env
|
|
5
|
+
# vars, and — since this gem ships with permissive-nothing defaults (see
|
|
6
|
+
# README's "Security hooks") — wire a real Authenticator/RateLimiter/etc. via
|
|
7
|
+
# a config file loaded with PORTAGE_UCP_CONFIG, the same way `rackup -r` or
|
|
8
|
+
# Sidekiq's `-r` work — see ../examples/portage_ucp.rb for a starting point.
|
|
9
|
+
# Without one, the server still starts but rejects every mutating call.
|
|
10
|
+
#
|
|
11
|
+
# WOOCOMMERCE_PAYMENT_METHOD is optional — only needed to call
|
|
12
|
+
# #complete_checkout (must match an installed, enabled WC gateway id).
|
|
13
|
+
|
|
14
|
+
require "portage/ucp"
|
|
15
|
+
require "portage/ucp/woocommerce"
|
|
16
|
+
|
|
17
|
+
require ENV["PORTAGE_UCP_CONFIG"] if ENV["PORTAGE_UCP_CONFIG"]
|
|
18
|
+
|
|
19
|
+
client = Portage::Ucp::WooCommerce::Client.new(
|
|
20
|
+
site_url: ENV.fetch("WOOCOMMERCE_SITE_URL"),
|
|
21
|
+
consumer_key: ENV.fetch("WOOCOMMERCE_CONSUMER_KEY"),
|
|
22
|
+
consumer_secret: ENV.fetch("WOOCOMMERCE_CONSUMER_SECRET")
|
|
23
|
+
)
|
|
24
|
+
adapter = Portage::Ucp::WooCommerce::Adapter.new(
|
|
25
|
+
client: client,
|
|
26
|
+
site_url: ENV.fetch("WOOCOMMERCE_SITE_URL"),
|
|
27
|
+
currency: ENV.fetch("WOOCOMMERCE_CURRENCY", "USD"),
|
|
28
|
+
payment_method: ENV.fetch("WOOCOMMERCE_PAYMENT_METHOD", nil)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
Portage::Ucp::Mcp::Server.build(adapter: adapter).start
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
module Portage
|
|
4
|
+
module Ucp
|
|
5
|
+
module WooCommerce
|
|
6
|
+
# Generic Portage::Ucp::Adapter over a WooCommerce site's Admin REST
|
|
7
|
+
# API v3 (catalog, order) and Store API v1 (cart, checkout) — no
|
|
8
|
+
# merchant-specific business logic, same posture as
|
|
9
|
+
# Portage::Ucp::Shopify::Adapter.
|
|
10
|
+
#
|
|
11
|
+
# Deliberately doesn't override `link_identity`: WooCommerce/WordPress
|
|
12
|
+
# user auth is a separate concern from the store-owner Admin keys and
|
|
13
|
+
# anonymous Store API session this gem uses, so this generic adapter
|
|
14
|
+
# leaves it unimplemented — Capability#advertised_for? simply won't
|
|
15
|
+
# advertise dev.ucp.shopping.identity for this adapter, not a 500.
|
|
16
|
+
#
|
|
17
|
+
# Catalog and Order are read through the Admin API (the Store API has
|
|
18
|
+
# no product search and can't look up an arbitrary past order without
|
|
19
|
+
# a customer session). Cart and Checkout are read/written through the
|
|
20
|
+
# Store API — Admin's REST v3 has no "Cart" resource at all, only
|
|
21
|
+
# Orders, and an Order only exists once checkout completes.
|
|
22
|
+
#
|
|
23
|
+
# IMPORTANT CAVEAT: #complete_checkout calls the Store API's
|
|
24
|
+
# `/checkout` endpoint with `payment_method` (configured at
|
|
25
|
+
# initialization, since it must match an installed, enabled WC
|
|
26
|
+
# payment gateway id) and `payment_data` built from `payment_token`.
|
|
27
|
+
# The exact `payment_data` key(s) a given gateway expects are gateway-
|
|
28
|
+
# specific (e.g. Stripe's block-based gateway reads a different key
|
|
29
|
+
# than PayPal's) and this hasn't been confirmed against a live site
|
|
30
|
+
# with a real gateway installed — same "needs confirming against a
|
|
31
|
+
# live store" posture as Portage::Ucp::Shopify::Adapter's own payment
|
|
32
|
+
# step.
|
|
33
|
+
class Adapter < Portage::Ucp::Adapter
|
|
34
|
+
# The Store API takes no idempotency key natively, so §9a dedup comes
|
|
35
|
+
# from Support::Idempotency's in-process table.
|
|
36
|
+
include Portage::Ucp::Support::Idempotency
|
|
37
|
+
# Store API cart and checkout share one underlying session (keyed by
|
|
38
|
+
# Cart-Token) with no status of its own, and WC orders don't link
|
|
39
|
+
# back to the Cart-Token that produced them (see Mapper.order) —
|
|
40
|
+
# both are tracked adapter-side via Support::CheckoutState.
|
|
41
|
+
include Portage::Ucp::Support::CheckoutState
|
|
42
|
+
# The Admin API answers a missing product or order with a 404 rather
|
|
43
|
+
# than an empty body, which UCP's reads report as nil.
|
|
44
|
+
include Portage::Ucp::Support::NotFound
|
|
45
|
+
|
|
46
|
+
def initialize(client:, site_url:, currency:, payment_method: nil, payment_data_key: "token")
|
|
47
|
+
super()
|
|
48
|
+
@client = client
|
|
49
|
+
@site_url = site_url.chomp("/")
|
|
50
|
+
@currency = currency
|
|
51
|
+
@payment_method = payment_method
|
|
52
|
+
@payment_data_key = payment_data_key
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def search_catalog(query:, limit:)
|
|
56
|
+
data = @client.admin_get("/products?search=#{URI.encode_www_form_component(query)}&per_page=#{limit}")
|
|
57
|
+
data.map { |node| Mapper.product(node, currency: @currency) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def get_product(product_id:)
|
|
61
|
+
nil_on_not_found do
|
|
62
|
+
node = @client.admin_get("/products/#{product_id}")
|
|
63
|
+
node["id"] ? Mapper.product(with_variations(node), currency: @currency) : nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_cart(cart_id:)
|
|
68
|
+
Mapper.cart(fetch_cart_node, id: cart_id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create_cart(line_items:, idempotency_key:)
|
|
72
|
+
dedup(idempotency_key) { Mapper.cart(replace_cart_lines(line_items), id: @client.cart_token) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Full replacement, same rationale as Portage::Ucp::Shopify::Adapter
|
|
76
|
+
# #update_cart: the Store API has no atomic "replace all lines"
|
|
77
|
+
# either, so this removes every current line then adds the desired
|
|
78
|
+
# ones back.
|
|
79
|
+
def update_cart(cart_id:, line_items:, idempotency_key:)
|
|
80
|
+
dedup(idempotency_key) { Mapper.cart(replace_cart_lines(line_items), id: cart_id) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The Store API's cart is tied to a session, not a deletable
|
|
84
|
+
# resource with its own lifecycle — there's no cancellation call,
|
|
85
|
+
# so this clears every line and returns the now-empty cart, the
|
|
86
|
+
# closest real equivalent.
|
|
87
|
+
def cancel_cart(cart_id:, idempotency_key:)
|
|
88
|
+
dedup(idempotency_key) { Mapper.cart(replace_cart_lines([]), id: cart_id) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def create_checkout(line_items:, idempotency_key:)
|
|
92
|
+
dedup(idempotency_key) do
|
|
93
|
+
node = replace_cart_lines(line_items)
|
|
94
|
+
record_checkout_status(@client.cart_token, "incomplete")
|
|
95
|
+
Mapper.checkout(node, id: @client.cart_token, status: "incomplete")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def get_checkout(checkout_id:)
|
|
100
|
+
Mapper.checkout(fetch_cart_node, id: checkout_id, status: checkout_status(checkout_id))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Full replacement, same rationale as #update_cart.
|
|
104
|
+
def update_checkout(checkout_id:, line_items:, idempotency_key:)
|
|
105
|
+
dedup(idempotency_key) do
|
|
106
|
+
node = replace_cart_lines(line_items)
|
|
107
|
+
record_checkout_status(checkout_id, "incomplete")
|
|
108
|
+
Mapper.checkout(node, id: checkout_id, status: "incomplete")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def complete_checkout(checkout_id:, payment_token:, idempotency_key:)
|
|
113
|
+
dedup(idempotency_key) { submit_checkout(checkout_id, payment_token) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The Store API has no cancellation endpoint for an in-progress
|
|
117
|
+
# checkout either (see #cancel_cart) — this just marks the tracked
|
|
118
|
+
# status canceled without touching the underlying cart contents.
|
|
119
|
+
def cancel_checkout(checkout_id:, idempotency_key:)
|
|
120
|
+
dedup(idempotency_key) do
|
|
121
|
+
record_checkout_status(checkout_id, "canceled")
|
|
122
|
+
Mapper.checkout(fetch_cart_node, id: checkout_id, status: "canceled")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def get_order(order_id:)
|
|
127
|
+
nil_on_not_found do
|
|
128
|
+
node = @client.admin_get("/orders/#{order_id}")
|
|
129
|
+
next nil unless node["id"]
|
|
130
|
+
|
|
131
|
+
Mapper.order(node, site_url: @site_url, checkout_id: checkout_id_for(order_id))
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def fetch_cart_node
|
|
138
|
+
@client.store_get("/cart")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# A variable product's Admin resource only lists variation *ids*
|
|
142
|
+
# (`variations: [123, 124]`) — fetching the full variation objects
|
|
143
|
+
# (price, stock, attribute choices) is a second call, only made for
|
|
144
|
+
# #get_product's single-product path, not #search_catalog's list
|
|
145
|
+
# (an N+1 fetch per search result would be too expensive).
|
|
146
|
+
def with_variations(node)
|
|
147
|
+
return node unless node["type"] == "variable" && node["variations"]&.any?
|
|
148
|
+
|
|
149
|
+
node.merge("variations_detail" => @client.admin_get("/products/#{node['id']}/variations?per_page=100"))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def cart_lines(line_items)
|
|
153
|
+
line_items.map { |li| { id: li[:product_id].to_i, quantity: li[:quantity] } }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def replace_cart_lines(line_items)
|
|
157
|
+
fetch_cart_node["items"].each { |item| @client.store_post("/cart/remove-item", { key: item["key"] }) }
|
|
158
|
+
cart_lines(line_items).each { |line| @client.store_post("/cart/add-item", line) }
|
|
159
|
+
fetch_cart_node
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# See the class-level CAVEAT: `payment_data` uses a single
|
|
163
|
+
# configurable key (`payment_data_key:`, default "token") rather
|
|
164
|
+
# than a confirmed gateway-specific shape.
|
|
165
|
+
def submit_checkout(checkout_id, payment_token)
|
|
166
|
+
raise Portage::Ucp::WooCommerce::Error, "no payment_method configured on this Adapter" unless @payment_method
|
|
167
|
+
|
|
168
|
+
data = @client.store_post("/checkout", {
|
|
169
|
+
payment_method: @payment_method,
|
|
170
|
+
payment_data: [{ key: @payment_data_key, value: payment_token }]
|
|
171
|
+
})
|
|
172
|
+
record_checkout_status(checkout_id, "completed")
|
|
173
|
+
order = build_order_confirmation(data, checkout_id)
|
|
174
|
+
Mapper.checkout(data, id: checkout_id, status: "completed", order: order)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Same order-received URL pattern as Mapper.order's `permalink_url` —
|
|
178
|
+
# the Store API's checkout response hands back order_id/order_key
|
|
179
|
+
# directly, so there's no need for a second Admin fetch just to build
|
|
180
|
+
# this link.
|
|
181
|
+
def build_order_confirmation(data, checkout_id)
|
|
182
|
+
return unless data["order_id"]
|
|
183
|
+
|
|
184
|
+
record_order_checkout(data["order_id"], checkout_id)
|
|
185
|
+
Portage::Ucp::OrderConfirmation.new(
|
|
186
|
+
id: data["order_id"].to_s,
|
|
187
|
+
permalink_url: "#{@site_url}/checkout/order-received/#{data['order_id']}/?key=#{data['order_key']}"
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Portage
|
|
5
|
+
module Ucp
|
|
6
|
+
module WooCommerce
|
|
7
|
+
# Minimal REST client over a WooCommerce site's two distinct APIs.
|
|
8
|
+
#
|
|
9
|
+
# Deliberately plain Net::HTTP, not the `woocommerce-api` gem — a
|
|
10
|
+
# generic adapter that any Ruby app can drop in only needs a site URL
|
|
11
|
+
# and a pair of keys, no framework coupling, trivially stubbable with
|
|
12
|
+
# WebMock.
|
|
13
|
+
#
|
|
14
|
+
# Like Shopify's Admin/Storefront split, WooCommerce has two separate
|
|
15
|
+
# APIs with two separate auth models:
|
|
16
|
+
#
|
|
17
|
+
# - Admin REST API (`/wp-json/wc/v3`): store-owner Basic Auth via a
|
|
18
|
+
# static consumer_key/consumer_secret pair generated once in
|
|
19
|
+
# wp-admin. No OAuth exchange, no expiry — unlike Shopify/Wix, there
|
|
20
|
+
# is no Portage::Ucp::WooCommerce::AccessTokenFetcher because there's
|
|
21
|
+
# nothing to exchange.
|
|
22
|
+
# - Store API (`/wp-json/wc/store/v1`): the public, unauthenticated
|
|
23
|
+
# cart/checkout API WooCommerce Blocks itself uses. It's session-
|
|
24
|
+
# based rather than token-based: the *first* request gets back a
|
|
25
|
+
# `Cart-Token` response header identifying an anonymous cart, which
|
|
26
|
+
# must be resent as a `Cart-Token` request header on every
|
|
27
|
+
# subsequent call to stay on the same cart. State-changing calls
|
|
28
|
+
# (POST/PUT/DELETE) also require a `Nonce` header, sourced the same
|
|
29
|
+
# way from an earlier response. This client tracks both headers
|
|
30
|
+
# in-instance so callers don't have to.
|
|
31
|
+
class Client
|
|
32
|
+
include Portage::Ucp::Support::HttpClient
|
|
33
|
+
|
|
34
|
+
def initialize(site_url:, consumer_key:, consumer_secret:)
|
|
35
|
+
@site_url = site_url.chomp("/")
|
|
36
|
+
@consumer_key = consumer_key
|
|
37
|
+
@consumer_secret = consumer_secret
|
|
38
|
+
@cart_token = nil
|
|
39
|
+
@nonce = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :cart_token
|
|
43
|
+
|
|
44
|
+
def admin_get(path)
|
|
45
|
+
admin_request(Net::HTTP::Get, path)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def admin_post(path, body = {})
|
|
49
|
+
admin_request(Net::HTTP::Post, path, body)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def store_get(path)
|
|
53
|
+
store_request(Net::HTTP::Get, path)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def store_post(path, body = {})
|
|
57
|
+
store_request(Net::HTTP::Post, path, body)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def store_delete(path)
|
|
61
|
+
store_request(Net::HTTP::Delete, path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def admin_request(http_method, path, body = nil)
|
|
67
|
+
json_request(http_method, "#{@site_url}/wp-json/wc/v3#{path}",
|
|
68
|
+
body: body, basic_auth: [@consumer_key, @consumer_secret])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Threads `Cart-Token` on every call (once one's been seen) and
|
|
72
|
+
# `Nonce` on writes — see the class-level comment. Both are
|
|
73
|
+
# refreshed from whatever the response hands back, since WooCommerce
|
|
74
|
+
# can rotate the nonce between requests, which is why this reads the
|
|
75
|
+
# raw response before parsing it.
|
|
76
|
+
def store_request(http_method, path, body = nil)
|
|
77
|
+
headers = {}
|
|
78
|
+
headers["Cart-Token"] = @cart_token if @cart_token
|
|
79
|
+
headers["Nonce"] = @nonce if @nonce && http_method != Net::HTTP::Get
|
|
80
|
+
response = json_request(http_method, "#{@site_url}/wp-json/wc/store/v1#{path}",
|
|
81
|
+
body: body, headers: headers, raw: true)
|
|
82
|
+
|
|
83
|
+
@cart_token = response["Cart-Token"] if response["Cart-Token"]
|
|
84
|
+
@nonce = response["Nonce"] if response["Nonce"]
|
|
85
|
+
parse!(response)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def api_error_class = Portage::Ucp::WooCommerce::ApiError
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Portage
|
|
2
|
+
module Ucp
|
|
3
|
+
module WooCommerce
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Raised for any non-2xx response from either the Admin REST API (v3,
|
|
7
|
+
# Basic Auth) or the Store API (v1, cart-token session) — both surface
|
|
8
|
+
# errors the same way: a non-2xx status with a JSON `{code, message}`
|
|
9
|
+
# body (`rest_*` codes on the Admin side, `woocommerce_rest_*`/
|
|
10
|
+
# `woocommerce_store_api_*` on the Store side).
|
|
11
|
+
class ApiError < Error
|
|
12
|
+
include Portage::Ucp::Support::ApiError
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def detail(body)
|
|
17
|
+
body["message"] || body
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
module Portage
|
|
2
|
+
module Ucp
|
|
3
|
+
module WooCommerce
|
|
4
|
+
# Converts WooCommerce Admin REST v3 (products, orders) and Store API
|
|
5
|
+
# v1 (cart, checkout) response bodies into the protocol-layer value
|
|
6
|
+
# objects from Portage::Ucp::ValueObjects — nothing WooCommerce-shaped
|
|
7
|
+
# is allowed to leak past this file.
|
|
8
|
+
#
|
|
9
|
+
# `currency` is threaded in by the caller everywhere it's needed: the
|
|
10
|
+
# Admin product resource doesn't carry a currency field at all (it's a
|
|
11
|
+
# site-wide setting, not per-product), unlike the Store API's cart/
|
|
12
|
+
# checkout responses, which do include one (`totals.currency_code`).
|
|
13
|
+
module Mapper
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def money(amount, currency)
|
|
17
|
+
Portage::Ucp::Support::Amounts.money(amount, currency)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def minor_units(amount)
|
|
21
|
+
Portage::Ucp::Support::Amounts.decimal_to_minor(amount)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# `node["variations_detail"]` is adapter-populated, not a real WC
|
|
25
|
+
# field: the Admin product resource only lists variation *ids* for a
|
|
26
|
+
# variable product (`variations: [123, 124]`), so fetching full
|
|
27
|
+
# variation objects is a second request the Adapter makes and merges
|
|
28
|
+
# in before calling this — Mapper stays pure translation, no I/O.
|
|
29
|
+
def product(node, currency:)
|
|
30
|
+
Portage::Ucp::Product.new(
|
|
31
|
+
id: node["id"].to_s,
|
|
32
|
+
title: node["name"],
|
|
33
|
+
description: node["description"],
|
|
34
|
+
price: money(node["price"], currency),
|
|
35
|
+
available: node["stock_status"] != "outofstock",
|
|
36
|
+
variants: variants(node, currency),
|
|
37
|
+
url: node["permalink"]
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A simple (non-variable) product has no real variants — it's its
|
|
42
|
+
# own single implicit variant, same as a single-variant Shopify
|
|
43
|
+
# product using that variant's own id.
|
|
44
|
+
def variants(node, currency)
|
|
45
|
+
detail = node["variations_detail"]
|
|
46
|
+
unless detail
|
|
47
|
+
return [{ id: node["id"].to_s, title: node["name"],
|
|
48
|
+
available: node["stock_status"] != "outofstock", price: money(node["price"], currency) }]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
detail.map { |v| variant(v, currency) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def variant(node, currency)
|
|
55
|
+
title = (node["attributes"] || []).map { |a| a["option"] }.join(" / ")
|
|
56
|
+
{ id: node["id"].to_s, title: title, available: node["stock_status"] != "outofstock",
|
|
57
|
+
price: money(node["price"], currency) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `id:` is caller-supplied rather than read off the response body:
|
|
61
|
+
# the Store API cart has no in-body resource id, only a `cart_hash`
|
|
62
|
+
# change-detection fingerprint. What actually identifies "this cart"
|
|
63
|
+
# across calls is the opaque `Cart-Token` the session is keyed by
|
|
64
|
+
# (see Portage::Ucp::WooCommerce::Client) — the Adapter passes that
|
|
65
|
+
# token through as `id:`.
|
|
66
|
+
def cart(node, id:)
|
|
67
|
+
currency = node.dig("totals", "currency_code")
|
|
68
|
+
Portage::Ucp::Cart.new(
|
|
69
|
+
id: id,
|
|
70
|
+
line_items: (node["items"] || []).map { |n| cart_line_item(n, currency) },
|
|
71
|
+
currency: currency,
|
|
72
|
+
totals: totals(node["totals"] || {})
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# `status` isn't a Store API cart field either — the Store API's
|
|
77
|
+
# cart *is* the checkout, same as Shopify's Cart-as-Checkout, so the
|
|
78
|
+
# adapter tracks status itself and passes it in here, same rationale
|
|
79
|
+
# as `id:` above.
|
|
80
|
+
def checkout(node, id:, status:, order: nil)
|
|
81
|
+
currency = node.dig("totals", "currency_code")
|
|
82
|
+
Portage::Ucp::Checkout.new(
|
|
83
|
+
id: id,
|
|
84
|
+
status: status,
|
|
85
|
+
line_items: (node["items"] || []).map { |n| cart_line_item(n, currency) },
|
|
86
|
+
currency: currency,
|
|
87
|
+
totals: totals(node["totals"] || {}),
|
|
88
|
+
links: [],
|
|
89
|
+
order: order
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def cart_line_item(node, _currency)
|
|
94
|
+
unit_price = minor_units_from_subunits(node.dig("prices", "price"), node.dig("prices", "currency_minor_unit"))
|
|
95
|
+
line_total = unit_price * node["quantity"]
|
|
96
|
+
Portage::Ucp::LineItem.new(
|
|
97
|
+
id: node["key"],
|
|
98
|
+
item: Portage::Ucp::Item.new(id: node["id"].to_s, title: node["name"], price: unit_price,
|
|
99
|
+
image_url: node.dig("images", 0, "thumbnail")),
|
|
100
|
+
quantity: node["quantity"],
|
|
101
|
+
totals: Portage::Ucp::Support::Totals.line(line_total)
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Store API money fields are already minor-unit integers as strings
|
|
106
|
+
# (e.g. "500" for $5.00), tagged with `currency_minor_unit` (usually
|
|
107
|
+
# 2) — unlike the Admin API's decimal strings, there's no BigDecimal
|
|
108
|
+
# conversion needed here, just an integer parse.
|
|
109
|
+
def minor_units_from_subunits(amount, _minor_unit)
|
|
110
|
+
Portage::Ucp::Support::Amounts.subunits_to_minor(amount)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def totals(node)
|
|
114
|
+
Portage::Ucp::Support::Totals.summary(
|
|
115
|
+
subtotal: minor_units_from_subunits(node["total_items"], nil),
|
|
116
|
+
tax: minor_units_from_subunits(node["total_tax"], nil),
|
|
117
|
+
total: minor_units_from_subunits(node["total_price"], nil)
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# WooCommerce core has no per-line-item fulfillment tracking (that's
|
|
122
|
+
# a shipment-tracking-plugin concern) — every line is given the same
|
|
123
|
+
# coarse status derived from the order's own top-level `status`,
|
|
124
|
+
# rather than a real per-line signal.
|
|
125
|
+
ORDER_LINE_ITEM_STATUS = {
|
|
126
|
+
"completed" => "fulfilled", "processing" => "processing", "pending" => "processing",
|
|
127
|
+
"on-hold" => "processing", "cancelled" => "removed", "refunded" => "removed", "failed" => "removed"
|
|
128
|
+
}.freeze
|
|
129
|
+
|
|
130
|
+
# `permalink_url` is built from WooCommerce's documented order-
|
|
131
|
+
# received URL pattern (`/checkout/order-received/{id}/?key={key}`)
|
|
132
|
+
# — the Admin order resource doesn't return a ready-made link the
|
|
133
|
+
# way Shopify's `statusPageUrl` does. `checkout_id` isn't a WC order
|
|
134
|
+
# field at all — nothing on Order links back to the Cart-Token that
|
|
135
|
+
# produced it, so the Adapter resolves it itself (tracked in-process
|
|
136
|
+
# from the #complete_checkout call that created the order, since the
|
|
137
|
+
# Store API's checkout response hands back the new order id right
|
|
138
|
+
# then) and passes it in here, same as Shopify's Mapper.order.
|
|
139
|
+
def order(node, site_url:, checkout_id: "")
|
|
140
|
+
currency = node["currency"]
|
|
141
|
+
subtotal = minor_units(node["total"]) - minor_units(node["total_tax"])
|
|
142
|
+
status = Portage::Ucp::Support::LineItemStatus.from_table(ORDER_LINE_ITEM_STATUS, node["status"])
|
|
143
|
+
Portage::Ucp::Order.new(
|
|
144
|
+
id: node["id"].to_s,
|
|
145
|
+
checkout_id: checkout_id,
|
|
146
|
+
permalink_url: "#{site_url}/checkout/order-received/#{node['id']}/?key=#{node['order_key']}",
|
|
147
|
+
line_items: (node["line_items"] || []).map { |n| order_line_item(n, currency, status) },
|
|
148
|
+
fulfillment: Portage::Ucp::Fulfillment.new,
|
|
149
|
+
currency: currency,
|
|
150
|
+
totals: Portage::Ucp::Support::Totals.summary(subtotal: subtotal, total: minor_units(node["total"]))
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def order_line_item(node, currency, status)
|
|
155
|
+
quantity = node["quantity"]
|
|
156
|
+
fulfilled = Portage::Ucp::Support::LineItemStatus.fulfilled_quantity(status, quantity)
|
|
157
|
+
line_total = minor_units(node["total"])
|
|
158
|
+
Portage::Ucp::OrderLineItem.new(
|
|
159
|
+
id: node["id"].to_s,
|
|
160
|
+
item: Portage::Ucp::Item.new(id: (node["variation_id"] || node["product_id"]).to_s, title: node["name"],
|
|
161
|
+
price: money(node["price"], currency).amount_minor),
|
|
162
|
+
quantity: { original: quantity, total: quantity, fulfilled: fulfilled },
|
|
163
|
+
totals: Portage::Ucp::Support::Totals.line(line_total),
|
|
164
|
+
status: status
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: portage-ucp-woocommerce
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tom Whitbread
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-14 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: portage-ucp
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rspec
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.13'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.13'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rubocop
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.88'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.88'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: webmock
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.24'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.24'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: yard
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.9'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.9'
|
|
83
|
+
description: Implements Portage::Ucp::Adapter against a WooCommerce site's Admin REST
|
|
84
|
+
API v3 (catalog, order) and Store API v1 (cart, checkout). Generic only — no merchant-specific
|
|
85
|
+
business logic. Plain Net::HTTP, no woocommerce-api runtime dependency.
|
|
86
|
+
email:
|
|
87
|
+
executables:
|
|
88
|
+
- portage-ucp-woocommerce
|
|
89
|
+
extensions: []
|
|
90
|
+
extra_rdoc_files: []
|
|
91
|
+
files:
|
|
92
|
+
- CHANGELOG.md
|
|
93
|
+
- LICENSE
|
|
94
|
+
- README.md
|
|
95
|
+
- exe/portage-ucp-woocommerce
|
|
96
|
+
- lib/portage/ucp/woocommerce.rb
|
|
97
|
+
- lib/portage/ucp/woocommerce/adapter.rb
|
|
98
|
+
- lib/portage/ucp/woocommerce/client.rb
|
|
99
|
+
- lib/portage/ucp/woocommerce/errors.rb
|
|
100
|
+
- lib/portage/ucp/woocommerce/mapper.rb
|
|
101
|
+
- lib/portage/ucp/woocommerce/version.rb
|
|
102
|
+
homepage: https://github.com/tomtom87/Portage/tree/main/portage-ucp-woocommerce
|
|
103
|
+
licenses:
|
|
104
|
+
- MIT
|
|
105
|
+
metadata:
|
|
106
|
+
source_code_uri: https://github.com/tomtom87/Portage/tree/main/portage-ucp-woocommerce
|
|
107
|
+
changelog_uri: https://github.com/tomtom87/Portage/blob/main/portage-ucp-woocommerce/CHANGELOG.md
|
|
108
|
+
rubygems_mfa_required: 'true'
|
|
109
|
+
post_install_message:
|
|
110
|
+
rdoc_options: []
|
|
111
|
+
require_paths:
|
|
112
|
+
- lib
|
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '3.2'
|
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - ">="
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '0'
|
|
123
|
+
requirements: []
|
|
124
|
+
rubygems_version: 3.5.22
|
|
125
|
+
signing_key:
|
|
126
|
+
specification_version: 4
|
|
127
|
+
summary: WooCommerce adapter for portage-ucp — standard catalog/cart/checkout/order
|
|
128
|
+
over MCP and UCP
|
|
129
|
+
test_files: []
|