portage-ucp-wix 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-wix +24 -0
- data/lib/portage/ucp/wix/access_token_fetcher.rb +42 -0
- data/lib/portage/ucp/wix/adapter.rb +194 -0
- data/lib/portage/ucp/wix/client.rb +56 -0
- data/lib/portage/ucp/wix/errors.rb +22 -0
- data/lib/portage/ucp/wix/mapper.rb +152 -0
- data/lib/portage/ucp/wix/version.rb +7 -0
- data/lib/portage/ucp/wix.rb +7 -0
- metadata +130 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b3982d49401e0956a0ab63673941a7b943868d1ce104224e78b057b62b5befde
|
|
4
|
+
data.tar.gz: 6a0e9017b011b1f3b1182857865ad926a3e3839c09afd7fdb215eec8d3cbf843
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 75d3d83ea3bc81837f7ada0d3513faa85b1a37f878e99af219b291d3a8fc83482379d6116863f54b17ae8f14e0fe049651eb641159bbb73c0c17a1bab2d4398c
|
|
7
|
+
data.tar.gz: '008f09ea6d0e5da325ba2aa847f0604ef662c672ed60dceb4a6d964eba2c655bcd54bc43a31b92177373a03e63493ef03f168a4c173b272afa50b779a078db65'
|
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. Wix adapter against the Stores (catalog) and eCommerce
|
|
10
|
+
(cart, checkout, order) REST APIs.
|
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-wix
|
|
2
|
+
|
|
3
|
+
Wix adapter for [`portage-ucp`](../portage-ucp). Implements `Portage::Ucp::Adapter` against Wix's Stores (catalog) and eCommerce (cart, checkout, order) REST APIs. Generic only — no merchant-specific business logic. Plain `Net::HTTP`, no Wix SDK runtime dependency.
|
|
4
|
+
|
|
5
|
+
## What it covers
|
|
6
|
+
|
|
7
|
+
| UCP capability | Backing Wix API | Notes |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `dev.ucp.shopping.catalog` | Stores Catalog V1 | `search_catalog`, `get_product` |
|
|
10
|
+
| `dev.ucp.shopping.cart` | eCommerce Carts | `get_cart`, `create_cart`, `update_cart`, `cancel_cart` |
|
|
11
|
+
| `dev.ucp.shopping.checkout` | eCommerce Checkouts | `create_checkout`, `get_checkout`, `update_checkout`, `complete_checkout`, `cancel_checkout` |
|
|
12
|
+
| `dev.ucp.shopping.order` | eCommerce Orders | `get_order` |
|
|
13
|
+
| `dev.ucp.shopping.identity` | — | not implemented; Wix Members/visitor OAuth is a separate concern from the site-level app auth used here |
|
|
14
|
+
|
|
15
|
+
Unlike Shopify, Wix models Cart and Checkout as genuinely separate resources, and a Wix Order links back to its originating Checkout natively via `checkoutId` — no cart-token reconciliation search needed for `Order#checkout_id`.
|
|
16
|
+
|
|
17
|
+
Update/replace operations (`update_cart`, `update_checkout`) are full-replacement: Wix's add/remove-line-items endpoints aren't an atomic "replace all lines" 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
|
+
## ⚠️ Unverified against a live site
|
|
20
|
+
|
|
21
|
+
This adapter is built from Wix's documented REST API shapes but hasn't been run against a real Wix site. Before relying on it in production, confirm:
|
|
22
|
+
|
|
23
|
+
- **Catalog shape** — targets Stores Catalog **V1** (`priceData`, `stock`, `productPageUrl`, variant `choices`), not V3. If your site's app only has V3 access, `Mapper.product`/`.variant` need rewriting against V3's shape.
|
|
24
|
+
- **`#complete_checkout`** — calls Wix's `create-order` endpoint bare. Wix's documented flow expects payment to already be authorized through a connected payment provider (Wix Payments or a PSP) before an order is created; there's no confirmed Wix equivalent of Shopify's `cartPaymentUpdate` that accepts an arbitrary single-use token. `payment_token` is accepted for interface parity with other `Portage::Ucp` adapters but currently isn't sent anywhere — wiring real payment capture needs confirming against a live site with a configured payment provider.
|
|
25
|
+
- **Order fulfillment** — Wix order line items only expose a coarse `fulfillmentStatus` (not/partially/fully fulfilled quantities); `Order#fulfillment` is left as `{}` since real per-shipment tracking would need Wix's separate Fulfillments API, which isn't wired up here.
|
|
26
|
+
- **`Order#permalink_url`** — left blank; Wix's Orders API doesn't return a public order-status page URL.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# Gemfile
|
|
32
|
+
gem "portage-ucp-wix"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bundle install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Setup
|
|
40
|
+
|
|
41
|
+
You need a single site-scoped access token — unlike Shopify's split Admin/Storefront tokens, Wix's REST surface sits behind one token, already scoped to a site.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
require "portage/ucp/wix"
|
|
45
|
+
|
|
46
|
+
client = Portage::Ucp::Wix::Client.new(access_token: ENV.fetch("WIX_ACCESS_TOKEN"))
|
|
47
|
+
adapter = Portage::Ucp::Wix::Adapter.new(client: client)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Fetching an access token from an app's client credentials
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
fetcher = Portage::Ucp::Wix::AccessTokenFetcher.new(
|
|
54
|
+
client_id: ENV.fetch("WIX_CLIENT_ID"),
|
|
55
|
+
client_secret: ENV.fetch("WIX_CLIENT_SECRET"),
|
|
56
|
+
instance_id: ENV.fetch("WIX_INSTANCE_ID")
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
result = fetcher.fetch
|
|
60
|
+
result.access_token # => access_token to pass into Client.new
|
|
61
|
+
result.expires_in # => seconds until it needs refetching
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# Catalog
|
|
68
|
+
products = adapter.search_catalog(query: "hoodie", limit: 10)
|
|
69
|
+
product = adapter.get_product(product_id: products.first.id)
|
|
70
|
+
|
|
71
|
+
# Cart
|
|
72
|
+
cart = adapter.create_cart(
|
|
73
|
+
line_items: [{ product_id: product.variants.first[:id], quantity: 2 }],
|
|
74
|
+
idempotency_key: SecureRandom.uuid
|
|
75
|
+
)
|
|
76
|
+
cart = adapter.update_cart(cart_id: cart.id, line_items: [], idempotency_key: SecureRandom.uuid) # empties cart
|
|
77
|
+
|
|
78
|
+
# Checkout
|
|
79
|
+
checkout = adapter.create_checkout(
|
|
80
|
+
line_items: [{ product_id: product.variants.first[:id], quantity: 1 }],
|
|
81
|
+
idempotency_key: SecureRandom.uuid
|
|
82
|
+
)
|
|
83
|
+
checkout = adapter.complete_checkout(
|
|
84
|
+
checkout_id: checkout.id,
|
|
85
|
+
payment_token: single_use_token_from_payment_handler,
|
|
86
|
+
idempotency_key: SecureRandom.uuid
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Order
|
|
90
|
+
order = adapter.get_order(order_id: checkout.id) # only once linked post-completion
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Wiring into portage-ucp
|
|
94
|
+
|
|
95
|
+
Drop the adapter into a `Dispatcher` (or the MCP server) the same as any other backend:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
dispatcher = Portage::Ucp::Dispatcher.new(adapter: adapter)
|
|
99
|
+
|
|
100
|
+
dispatcher.call(
|
|
101
|
+
capability: "dev.ucp.shopping.cart",
|
|
102
|
+
action: "create",
|
|
103
|
+
arguments: { line_items: [{ product_id: variant_id, quantity: 1 }], idempotency_key: SecureRandom.uuid }
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
## Errors
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
Portage::Ucp::Wix::Error # base class
|
|
113
|
+
Portage::Ucp::Wix::ApiError # any non-2xx Wix REST response (bad auth, malformed request, business rejection)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
bundle exec rspec # tests (WebMock-stubbed, no live site needed)
|
|
120
|
+
bundle exec rubocop # lint
|
|
121
|
+
|
|
122
|
+
# fetch a real access_token for a dev site, via client_credentials
|
|
123
|
+
WIX_CLIENT_ID=... WIX_CLIENT_SECRET=... WIX_INSTANCE_ID=... \
|
|
124
|
+
bundle exec rake wix_access_token
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
[MIT](LICENSE) — Copyright (c) 2026 Tom Whitbread.
|
data/exe/portage-ucp-wix
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
# Only WIX_ACCESS_TOKEN is read here, not client_id/client_secret/
|
|
12
|
+
# instance_id: this exe expects a site-scoped token you've already fetched
|
|
13
|
+
# (e.g. via `bundle exec rake wix_access_token`) and refresh out-of-band,
|
|
14
|
+
# same as any other long-lived process holding a token with an expiry.
|
|
15
|
+
|
|
16
|
+
require "portage/ucp"
|
|
17
|
+
require "portage/ucp/wix"
|
|
18
|
+
|
|
19
|
+
require ENV["PORTAGE_UCP_CONFIG"] if ENV["PORTAGE_UCP_CONFIG"]
|
|
20
|
+
|
|
21
|
+
client = Portage::Ucp::Wix::Client.new(access_token: ENV.fetch("WIX_ACCESS_TOKEN"))
|
|
22
|
+
adapter = Portage::Ucp::Wix::Adapter.new(client: client)
|
|
23
|
+
|
|
24
|
+
Portage::Ucp::Mcp::Server.build(adapter: adapter).start
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Portage
|
|
5
|
+
module Ucp
|
|
6
|
+
module Wix
|
|
7
|
+
# Exchanges a Wix app's client_id/client_secret (plus the target site's
|
|
8
|
+
# instance_id) for a site-scoped access token via Wix's OAuth
|
|
9
|
+
# client_credentials grant.
|
|
10
|
+
#
|
|
11
|
+
# Wix apps authenticate server-to-server the same way Shopify custom
|
|
12
|
+
# apps now do: no static, copy-once token — client_id/client_secret get
|
|
13
|
+
# exchanged (and re-exchanged once expired) for the real access_token
|
|
14
|
+
# used by Portage::Ucp::Wix::Client. The one addition versus Shopify is
|
|
15
|
+
# `instance_id`: Wix's OAuth endpoint is one shared host
|
|
16
|
+
# (www.wixapis.com) serving every site, so the token has to be scoped
|
|
17
|
+
# to a specific site's app instance at exchange time rather than via a
|
|
18
|
+
# per-shop subdomain.
|
|
19
|
+
class AccessTokenFetcher
|
|
20
|
+
include Portage::Ucp::Support::TokenExchange
|
|
21
|
+
|
|
22
|
+
Result = Struct.new(:access_token, :expires_in, keyword_init: true)
|
|
23
|
+
|
|
24
|
+
ENDPOINT = "https://www.wixapis.com/oauth2/token".freeze
|
|
25
|
+
|
|
26
|
+
def initialize(client_id:, client_secret:, instance_id:)
|
|
27
|
+
@client_id = client_id
|
|
28
|
+
@client_secret = client_secret
|
|
29
|
+
@instance_id = instance_id
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fetch
|
|
33
|
+
body = exchange(ENDPOINT,
|
|
34
|
+
{ grant_type: "client_credentials", client_id: @client_id,
|
|
35
|
+
client_secret: @client_secret, instance_id: @instance_id },
|
|
36
|
+
error_class: Portage::Ucp::Wix::Error)
|
|
37
|
+
Result.new(access_token: body["access_token"], expires_in: body["expires_in"])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Portage
|
|
4
|
+
module Ucp
|
|
5
|
+
module Wix
|
|
6
|
+
# Generic Portage::Ucp::Adapter over Wix's Stores (catalog) and
|
|
7
|
+
# eCommerce (cart/checkout/order) REST APIs — no merchant-specific
|
|
8
|
+
# business logic, same posture as Portage::Ucp::Shopify::Adapter.
|
|
9
|
+
#
|
|
10
|
+
# Deliberately doesn't override `link_identity`: Wix's OAuth identity
|
|
11
|
+
# story (Wix Members/visitor auth) is a separate concern from the
|
|
12
|
+
# site-level app auth this gem uses for catalog/cart/checkout/order, so
|
|
13
|
+
# this generic adapter leaves it unimplemented — Capability#
|
|
14
|
+
# advertised_for? simply won't advertise dev.ucp.shopping.identity for
|
|
15
|
+
# this adapter, not a 500.
|
|
16
|
+
#
|
|
17
|
+
# Unlike Shopify, where Cart and Checkout are the same underlying
|
|
18
|
+
# object, Wix models them as genuinely separate resources with their
|
|
19
|
+
# own REST endpoints — so, unlike Shopify's Adapter, there's no shared
|
|
20
|
+
# create_cart_node/create_checkout_node plumbing between the two.
|
|
21
|
+
#
|
|
22
|
+
# IMPORTANT CAVEAT: #complete_checkout's call to Wix's "Create Order"
|
|
23
|
+
# endpoint hasn't been confirmed against a live site. Wix's documented
|
|
24
|
+
# checkout flow expects payment to already be authorized through a
|
|
25
|
+
# payment provider (Wix Payments or a connected PSP) before an order
|
|
26
|
+
# is created — there's no confirmed equivalent of Shopify's
|
|
27
|
+
# cartPaymentUpdate that accepts an arbitrary single-use payment_token
|
|
28
|
+
# directly. `payment_token` is accepted for interface parity with the
|
|
29
|
+
# other Portage::Ucp adapters but isn't currently sent anywhere; wiring
|
|
30
|
+
# real payment capture needs confirming against a live Wix site with a
|
|
31
|
+
# configured payment provider before this method is production-ready.
|
|
32
|
+
class Adapter < Portage::Ucp::Adapter
|
|
33
|
+
# Wix's fixed app id for its own Stores catalog, required on every
|
|
34
|
+
# cart/checkout line item's catalogReference so Wix knows which
|
|
35
|
+
# catalog `catalogItemId` belongs to.
|
|
36
|
+
STORES_APP_ID = "215238eb-22a5-4c36-9e7b-e7c08025e04e".freeze
|
|
37
|
+
|
|
38
|
+
# Wix's cart/checkout REST endpoints don't take an idempotency key
|
|
39
|
+
# natively, so §9a dedup comes from Support::Idempotency's in-process
|
|
40
|
+
# table.
|
|
41
|
+
include Portage::Ucp::Support::Idempotency
|
|
42
|
+
# Wix Checkout has no confirmed native status field, so status is
|
|
43
|
+
# tracked adapter-side via Support::CheckoutState. Only the status
|
|
44
|
+
# half is used here: unlike Shopify and WooCommerce, a Wix Order
|
|
45
|
+
# carries its originating `checkoutId` natively (see #get_order).
|
|
46
|
+
include Portage::Ucp::Support::CheckoutState
|
|
47
|
+
|
|
48
|
+
def initialize(client:)
|
|
49
|
+
super()
|
|
50
|
+
@client = client
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def search_catalog(query:, limit:)
|
|
54
|
+
body = { query: { filter: JSON.generate({ name: { "$contains" => query } }), paging: { limit: limit } } }
|
|
55
|
+
data = @client.post("/stores/v1/products/query", body)
|
|
56
|
+
(data["products"] || []).map { |node| Mapper.product(node) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def get_product(product_id:)
|
|
60
|
+
node = @client.get("/stores/v1/products/#{product_id}")["product"]
|
|
61
|
+
node && Mapper.product(node)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def get_cart(cart_id:)
|
|
65
|
+
node = fetch_cart_node(cart_id)
|
|
66
|
+
node && Mapper.cart(node)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def create_cart(line_items:, idempotency_key:)
|
|
70
|
+
dedup(idempotency_key) { Mapper.cart(create_cart_node(line_items)) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Full replacement, matching UCP's real cart semantics: this removes
|
|
74
|
+
# every current line then adds the desired ones back, same as
|
|
75
|
+
# Portage::Ucp::Shopify::Adapter#update_cart (Wix's add/remove-line-
|
|
76
|
+
# items endpoints aren't an atomic "replace all lines" either).
|
|
77
|
+
def update_cart(cart_id:, line_items:, idempotency_key:)
|
|
78
|
+
dedup(idempotency_key) { Mapper.cart(replace_cart_lines(cart_id, line_items)) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Wix carts can actually be deleted (unlike Shopify's, which just
|
|
82
|
+
# expire) — this deletes the cart, then returns its last-known state
|
|
83
|
+
# since there's nothing more to hand back once it's gone.
|
|
84
|
+
def cancel_cart(cart_id:, idempotency_key:)
|
|
85
|
+
dedup(idempotency_key) do
|
|
86
|
+
node = fetch_cart_node(cart_id)
|
|
87
|
+
@client.delete("/ecom/v1/carts/#{cart_id}")
|
|
88
|
+
Mapper.cart(node)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def create_checkout(line_items:, idempotency_key:)
|
|
93
|
+
dedup(idempotency_key) do
|
|
94
|
+
node = create_checkout_node(line_items)
|
|
95
|
+
record_checkout_status(node["id"], "incomplete")
|
|
96
|
+
Mapper.checkout(node, status: "incomplete")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def get_checkout(checkout_id:)
|
|
101
|
+
node = fetch_checkout_node(checkout_id)
|
|
102
|
+
node && Mapper.checkout(node, status: checkout_status(checkout_id))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Full replacement, same rationale as #update_cart.
|
|
106
|
+
def update_checkout(checkout_id:, line_items:, idempotency_key:)
|
|
107
|
+
dedup(idempotency_key) do
|
|
108
|
+
node = @client.patch("/ecom/v1/checkouts/#{checkout_id}", { lineItems: cart_lines(line_items) })["checkout"]
|
|
109
|
+
record_checkout_status(checkout_id, "incomplete")
|
|
110
|
+
Mapper.checkout(node, status: "incomplete")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# See the class-level CAVEAT: payment_token isn't currently sent to
|
|
115
|
+
# Wix — see this method's implementation note below.
|
|
116
|
+
def complete_checkout(checkout_id:, payment_token:, idempotency_key:)
|
|
117
|
+
dedup(idempotency_key) { submit_order(checkout_id, payment_token) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def cancel_checkout(checkout_id:, idempotency_key:)
|
|
121
|
+
dedup(idempotency_key) do
|
|
122
|
+
record_checkout_status(checkout_id, "canceled")
|
|
123
|
+
node = fetch_checkout_node(checkout_id)
|
|
124
|
+
Mapper.checkout(node, status: "canceled")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# `checkout_id` comes straight off the Wix Order's own `checkoutId`
|
|
129
|
+
# field — unlike Shopify, Wix's Order object links back to its
|
|
130
|
+
# originating Checkout natively, so there's no cart_token-style
|
|
131
|
+
# reconciliation search needed here.
|
|
132
|
+
def get_order(order_id:)
|
|
133
|
+
node = @client.get("/ecom/v1/orders/#{order_id}")["order"]
|
|
134
|
+
node && Mapper.order(node)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def fetch_cart_node(cart_id)
|
|
140
|
+
@client.get("/ecom/v1/carts/#{cart_id}")["cart"]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def fetch_checkout_node(checkout_id)
|
|
144
|
+
@client.get("/ecom/v1/checkouts/#{checkout_id}")["checkout"]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def cart_lines(line_items)
|
|
148
|
+
line_items.map do |li|
|
|
149
|
+
{ catalogReference: { catalogItemId: li[:product_id], appId: STORES_APP_ID },
|
|
150
|
+
quantity: li[:quantity] }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def create_cart_node(line_items)
|
|
155
|
+
@client.post("/ecom/v1/carts", { lineItems: cart_lines(line_items) })["cart"]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def create_checkout_node(line_items)
|
|
159
|
+
@client.post("/ecom/v1/checkouts", { lineItems: cart_lines(line_items) })["checkout"]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def replace_cart_lines(cart_id, line_items)
|
|
163
|
+
current_ids = fetch_cart_node(cart_id)["lineItems"].map { |n| n["id"] }
|
|
164
|
+
unless current_ids.empty?
|
|
165
|
+
@client.post("/ecom/v1/carts/#{cart_id}/remove-line-items", { lineItemIds: current_ids })
|
|
166
|
+
end
|
|
167
|
+
return fetch_cart_node(cart_id) if line_items.empty?
|
|
168
|
+
|
|
169
|
+
@client.post("/ecom/v1/carts/#{cart_id}/add-line-items", { lineItems: cart_lines(line_items) })["cart"]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# `payment_token` is deliberately not sent anywhere yet — see the
|
|
173
|
+
# class-level CAVEAT. `create-order` is called bare; a checkout that
|
|
174
|
+
# hasn't actually been paid through a real Wix payment provider will
|
|
175
|
+
# be rejected by Wix itself here, surfacing as an ApiError rather
|
|
176
|
+
# than silently succeeding.
|
|
177
|
+
def submit_order(checkout_id, _payment_token)
|
|
178
|
+
node = fetch_checkout_node(checkout_id)
|
|
179
|
+
raise Portage::Ucp::Wix::Error, "checkout #{checkout_id} not found" unless node
|
|
180
|
+
|
|
181
|
+
data = @client.post("/ecom/v1/checkouts/#{checkout_id}/create-order", {})
|
|
182
|
+
order_id = data["orderId"]
|
|
183
|
+
status = order_id ? "completed" : "complete_in_progress"
|
|
184
|
+
record_checkout_status(checkout_id, status)
|
|
185
|
+
# Same "not information-complete but schema-valid" posture as
|
|
186
|
+
# Mapper.order's own blank permalink_url — Wix's create-order
|
|
187
|
+
# response has no storefront order-status link to hand back.
|
|
188
|
+
order = order_id && Portage::Ucp::OrderConfirmation.new(id: order_id, permalink_url: "")
|
|
189
|
+
Mapper.checkout(node, status: status, order: order)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Portage
|
|
5
|
+
module Ucp
|
|
6
|
+
module Wix
|
|
7
|
+
# Minimal REST client over the Wix APIs (www.wixapis.com).
|
|
8
|
+
#
|
|
9
|
+
# Deliberately plain Net::HTTP, not the `wix-ruby-sdk` (there isn't a
|
|
10
|
+
# first-party one) — a generic adapter that any Ruby app can drop in
|
|
11
|
+
# only needs one bearer-style access token and a handful of JSON
|
|
12
|
+
# endpoints, no framework coupling, trivially stubbable with WebMock.
|
|
13
|
+
#
|
|
14
|
+
# Unlike Shopify's split Admin/Storefront APIs with separate tokens,
|
|
15
|
+
# Wix's REST surface (Stores catalog, eCommerce carts/checkouts/orders)
|
|
16
|
+
# is a single API behind one access token — an app's client_credentials
|
|
17
|
+
# token is already scoped to one site via `instance_id` at fetch time
|
|
18
|
+
# (see Portage::Ucp::Wix::AccessTokenFetcher), so there's nothing here
|
|
19
|
+
# analogous to Shopify's per-call token selection.
|
|
20
|
+
class Client
|
|
21
|
+
include Portage::Ucp::Support::HttpClient
|
|
22
|
+
|
|
23
|
+
BASE_URL = "https://www.wixapis.com".freeze
|
|
24
|
+
|
|
25
|
+
def initialize(access_token:)
|
|
26
|
+
@access_token = access_token
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def get(path)
|
|
30
|
+
request(Net::HTTP::Get, path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def post(path, body = {})
|
|
34
|
+
request(Net::HTTP::Post, path, body)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def patch(path, body = {})
|
|
38
|
+
request(Net::HTTP::Patch, path, body)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def delete(path)
|
|
42
|
+
request(Net::HTTP::Delete, path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def request(http_method, path, body = nil)
|
|
48
|
+
json_request(http_method, "#{BASE_URL}#{path}", body: body,
|
|
49
|
+
headers: { "Authorization" => @access_token })
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def api_error_class = Portage::Ucp::Wix::ApiError
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Portage
|
|
2
|
+
module Ucp
|
|
3
|
+
module Wix
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Raised for any non-2xx response from a Wix REST call. Unlike Shopify's
|
|
7
|
+
# GraphQL/userErrors split, Wix's REST APIs report both transport-level
|
|
8
|
+
# rejections (bad auth, malformed request) and business rejections
|
|
9
|
+
# (e.g. "line item not found") the same way — a non-2xx status with a
|
|
10
|
+
# JSON error body — so one error class covers both.
|
|
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,152 @@
|
|
|
1
|
+
module Portage
|
|
2
|
+
module Ucp
|
|
3
|
+
module Wix
|
|
4
|
+
# Converts Wix Stores/eCommerce REST response bodies into the
|
|
5
|
+
# protocol-layer value objects from Portage::Ucp::ValueObjects
|
|
6
|
+
# (Product/Cart/LineItem/Checkout/Order/Money) — nothing Wix-shaped is
|
|
7
|
+
# allowed to leak past this file.
|
|
8
|
+
#
|
|
9
|
+
# Targets the Wix Stores Catalog V1 product shape (`priceData`,
|
|
10
|
+
# `stock`, `productPageUrl`) rather than V3 — V1's fields are the
|
|
11
|
+
# stable, longer-documented ones. A merchant on V3-only endpoints would
|
|
12
|
+
# need their own Mapper.
|
|
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
|
+
def product(node)
|
|
25
|
+
currency = node.dig("priceData", "currency")
|
|
26
|
+
Portage::Ucp::Product.new(
|
|
27
|
+
id: node["id"],
|
|
28
|
+
title: node["name"],
|
|
29
|
+
description: node["description"],
|
|
30
|
+
price: money(node.dig("priceData", "price"), currency),
|
|
31
|
+
available: node.dig("stock", "inStock") != false,
|
|
32
|
+
variants: (node["variants"] || []).map { |v| variant(v, node["name"], currency) },
|
|
33
|
+
url: product_url(node["productPageUrl"])
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def product_url(page)
|
|
38
|
+
return nil unless page && page["base"]
|
|
39
|
+
|
|
40
|
+
"#{page['base']}#{page['path']}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A V1 variant's own identity is its `choices` (e.g. {"Size"=>"Large"})
|
|
44
|
+
# — there's no separate variant title field, so one is built from the
|
|
45
|
+
# choice values, falling back to the parent product's title for a
|
|
46
|
+
# single-variant (no options) product.
|
|
47
|
+
def variant(node, product_title, currency)
|
|
48
|
+
choices = node["choices"] || {}
|
|
49
|
+
title = choices.values.join(" / ")
|
|
50
|
+
title = product_title if title.empty?
|
|
51
|
+
{ id: node["id"], title: title, available: node.dig("stock", "inStock") != false,
|
|
52
|
+
price: money(node.dig("variant", "priceData", "price"), currency) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def cart(node)
|
|
56
|
+
Portage::Ucp::Cart.new(
|
|
57
|
+
id: node["id"],
|
|
58
|
+
line_items: (node["lineItems"] || []).map { |n| line_item(n) },
|
|
59
|
+
currency: node["currency"],
|
|
60
|
+
totals: totals(node["priceSummary"] || {})
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# `status` isn't reliably present on a Wix Checkout the way it is on
|
|
65
|
+
# a Shopify Cart/Checkout hybrid — the adapter tracks it itself
|
|
66
|
+
# across the create/update/complete/cancel lifecycle and passes it
|
|
67
|
+
# in here, same rationale as Shopify's Mapper.checkout.
|
|
68
|
+
def checkout(node, status:, order: nil)
|
|
69
|
+
Portage::Ucp::Checkout.new(
|
|
70
|
+
id: node["id"],
|
|
71
|
+
status: status,
|
|
72
|
+
line_items: (node["lineItems"] || []).map { |n| line_item(n) },
|
|
73
|
+
currency: node["currency"],
|
|
74
|
+
totals: totals(node["priceSummary"] || {}),
|
|
75
|
+
links: [],
|
|
76
|
+
order: order
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Shared by Cart and Checkout line items — both are Wix `lineItem`
|
|
81
|
+
# shapes with the same fields. There's no documented per-line total
|
|
82
|
+
# field (only a per-cart/checkout `priceSummary` aggregate), so the
|
|
83
|
+
# line total is unit price * quantity, same as Shopify's variant
|
|
84
|
+
# price * quantity for a line with no discounts applied.
|
|
85
|
+
def line_item(node)
|
|
86
|
+
unit_price = minor_units(node.dig("price", "amount"))
|
|
87
|
+
quantity = node["quantity"]
|
|
88
|
+
line_total = unit_price * quantity
|
|
89
|
+
Portage::Ucp::LineItem.new(
|
|
90
|
+
id: node["id"],
|
|
91
|
+
item: Portage::Ucp::Item.new(id: node.dig("catalogReference", "catalogItemId"),
|
|
92
|
+
title: node.dig("productName", "original"), price: unit_price),
|
|
93
|
+
quantity: quantity,
|
|
94
|
+
totals: Portage::Ucp::Support::Totals.line(line_total)
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Builds the top-level totals array from Wix's priceSummary.
|
|
99
|
+
def totals(summary)
|
|
100
|
+
Portage::Ucp::Support::Totals.summary(subtotal: minor_units(summary.dig("subtotal", "amount")),
|
|
101
|
+
tax: minor_units(summary.dig("tax", "amount")),
|
|
102
|
+
total: minor_units(summary.dig("total", "amount")))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Wix Order line items carry their own coarse fulfillmentStatus
|
|
106
|
+
# (NOT_FULFILLED/PARTIALLY_FULFILLED/FULFILLED/CANCELED) rather than
|
|
107
|
+
# Shopify's separate quantity/unfulfilledQuantity counters, so exact
|
|
108
|
+
# partial-fulfillment quantities aren't derivable from the Orders API
|
|
109
|
+
# alone (Wix's separate Fulfillments API has those, and isn't wired
|
|
110
|
+
# up here) — `fulfilled` is a best-effort all-or-nothing based on
|
|
111
|
+
# that status.
|
|
112
|
+
ORDER_LINE_ITEM_STATUS = {
|
|
113
|
+
"FULFILLED" => "fulfilled", "CANCELED" => "removed",
|
|
114
|
+
"PARTIALLY_FULFILLED" => "partial", "NOT_FULFILLED" => "processing"
|
|
115
|
+
}.freeze
|
|
116
|
+
|
|
117
|
+
# `permalink_url` is left blank — Wix's Orders API doesn't return a
|
|
118
|
+
# public order-status page URL (that's rendered on the buyer's Thank
|
|
119
|
+
# You page client-side, not exposed via this API).
|
|
120
|
+
def order(node)
|
|
121
|
+
subtotal_amount = minor_units(node.dig("priceSummary", "subtotal", "amount"))
|
|
122
|
+
total_amount = minor_units(node.dig("priceSummary", "total", "amount"))
|
|
123
|
+
Portage::Ucp::Order.new(
|
|
124
|
+
id: node["id"],
|
|
125
|
+
checkout_id: node["checkoutId"] || "",
|
|
126
|
+
permalink_url: "",
|
|
127
|
+
line_items: (node["lineItems"] || []).map { |n| order_line_item(n) },
|
|
128
|
+
fulfillment: Portage::Ucp::Fulfillment.new,
|
|
129
|
+
currency: node["currency"],
|
|
130
|
+
totals: Portage::Ucp::Support::Totals.summary(subtotal: subtotal_amount, total: total_amount)
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def order_line_item(node)
|
|
135
|
+
quantity = node["quantity"]
|
|
136
|
+
status = Portage::Ucp::Support::LineItemStatus.from_table(ORDER_LINE_ITEM_STATUS, node["fulfillmentStatus"])
|
|
137
|
+
fulfilled = Portage::Ucp::Support::LineItemStatus.fulfilled_quantity(status, quantity)
|
|
138
|
+
unit_price = minor_units(node.dig("price", "amount"))
|
|
139
|
+
line_total = unit_price * quantity
|
|
140
|
+
Portage::Ucp::OrderLineItem.new(
|
|
141
|
+
id: node["id"],
|
|
142
|
+
item: Portage::Ucp::Item.new(id: node.dig("catalogReference", "catalogItemId"),
|
|
143
|
+
title: node.dig("productName", "original"), price: unit_price),
|
|
144
|
+
quantity: { original: quantity, total: quantity, fulfilled: fulfilled },
|
|
145
|
+
totals: Portage::Ucp::Support::Totals.line(line_total),
|
|
146
|
+
status: status
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: portage-ucp-wix
|
|
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 Wix's Stores (catalog) and eCommerce
|
|
84
|
+
(cart, checkout, order) REST APIs. Generic only — no merchant-specific business
|
|
85
|
+
logic. Plain Net::HTTP, no wix SDK runtime dependency.
|
|
86
|
+
email:
|
|
87
|
+
executables:
|
|
88
|
+
- portage-ucp-wix
|
|
89
|
+
extensions: []
|
|
90
|
+
extra_rdoc_files: []
|
|
91
|
+
files:
|
|
92
|
+
- CHANGELOG.md
|
|
93
|
+
- LICENSE
|
|
94
|
+
- README.md
|
|
95
|
+
- exe/portage-ucp-wix
|
|
96
|
+
- lib/portage/ucp/wix.rb
|
|
97
|
+
- lib/portage/ucp/wix/access_token_fetcher.rb
|
|
98
|
+
- lib/portage/ucp/wix/adapter.rb
|
|
99
|
+
- lib/portage/ucp/wix/client.rb
|
|
100
|
+
- lib/portage/ucp/wix/errors.rb
|
|
101
|
+
- lib/portage/ucp/wix/mapper.rb
|
|
102
|
+
- lib/portage/ucp/wix/version.rb
|
|
103
|
+
homepage: https://github.com/tomtom87/Portage/tree/main/portage-ucp-wix
|
|
104
|
+
licenses:
|
|
105
|
+
- MIT
|
|
106
|
+
metadata:
|
|
107
|
+
source_code_uri: https://github.com/tomtom87/Portage/tree/main/portage-ucp-wix
|
|
108
|
+
changelog_uri: https://github.com/tomtom87/Portage/blob/main/portage-ucp-wix/CHANGELOG.md
|
|
109
|
+
rubygems_mfa_required: 'true'
|
|
110
|
+
post_install_message:
|
|
111
|
+
rdoc_options: []
|
|
112
|
+
require_paths:
|
|
113
|
+
- lib
|
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '3.2'
|
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
requirements: []
|
|
125
|
+
rubygems_version: 3.5.22
|
|
126
|
+
signing_key:
|
|
127
|
+
specification_version: 4
|
|
128
|
+
summary: Wix adapter for portage-ucp — standard catalog/cart/checkout/order over MCP
|
|
129
|
+
and UCP
|
|
130
|
+
test_files: []
|