@decocms/apps-magento 7.2.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.
- package/package.json +47 -0
- package/src/README.md +65 -0
- package/src/__tests__/cart.test.ts +139 -0
- package/src/__tests__/client.test.ts +252 -0
- package/src/__tests__/features.test.ts +51 -0
- package/src/__tests__/graphql.test.ts +183 -0
- package/src/__tests__/newsletter-subscribe.test.ts +86 -0
- package/src/__tests__/product-stockAlert.test.ts +80 -0
- package/src/__tests__/stringifySearchCriteria.test.ts +56 -0
- package/src/__tests__/transform.test.ts +346 -0
- package/src/__tests__/user-loader.test.ts +134 -0
- package/src/__tests__/wishlist-actions.test.ts +140 -0
- package/src/__tests__/wishlist-loader.test.ts +97 -0
- package/src/actions/newsletter/subscribe.ts +40 -0
- package/src/actions/product/stockAlert.ts +67 -0
- package/src/actions/wishlist/addItem.ts +57 -0
- package/src/actions/wishlist/removeItem.ts +59 -0
- package/src/client.ts +228 -0
- package/src/index.ts +11 -0
- package/src/loaders/cart.ts +64 -0
- package/src/loaders/features.ts +16 -0
- package/src/loaders/user.ts +60 -0
- package/src/loaders/wishlist.ts +29 -0
- package/src/middleware.ts +19 -0
- package/src/types.ts +51 -0
- package/src/utils/cacheTimeControl.ts +69 -0
- package/src/utils/client/types.ts +270 -0
- package/src/utils/constants.ts +101 -0
- package/src/utils/graphql-types.ts +99 -0
- package/src/utils/graphql.ts +155 -0
- package/src/utils/stringifySearchCriteria.ts +62 -0
- package/src/utils/transform.ts +283 -0
- package/src/utils/user.ts +16 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the current customer into a schema.org `Person` from
|
|
3
|
+
* Magento's `/customer/section/load?sections=customer,carbono-customer`
|
|
4
|
+
* endpoint, scoped by the visitor's PHPSESSID cookie.
|
|
5
|
+
*
|
|
6
|
+
* Ported from `deco-cx/apps/magento/loaders/user.ts`. The Fresh
|
|
7
|
+
* version used `clientAdmin["GET /:site/customer/section/load"]` with
|
|
8
|
+
* typed indexed routes — the TanStack/Node port uses `magentoFetch`
|
|
9
|
+
* (which already applies auth/origin headers for same-origin) plus an
|
|
10
|
+
* explicit `Cookie: PHPSESSID=…` header so Magento associates the
|
|
11
|
+
* request with the logged-in customer.
|
|
12
|
+
*
|
|
13
|
+
* Returns null when:
|
|
14
|
+
* - the session cookie is absent (anonymous visitor — expected),
|
|
15
|
+
* - the customer slice is missing or has no data_id (Magento returns
|
|
16
|
+
* {} for guest sessions),
|
|
17
|
+
* - the HTTP call throws or returns a non-2xx (defensive — the
|
|
18
|
+
* storefront just renders the logged-out UI).
|
|
19
|
+
*/
|
|
20
|
+
import type { Person } from "@decocms/apps-commerce/types";
|
|
21
|
+
import { getMagentoConfig, magentoFetch } from "../client";
|
|
22
|
+
import type { CustomerSectionLoad } from "../utils/client/types";
|
|
23
|
+
import { SESSION_COOKIE } from "../utils/constants";
|
|
24
|
+
import { getUserCookie } from "../utils/user";
|
|
25
|
+
|
|
26
|
+
export default async function user(_props: unknown, req: Request): Promise<Person | null> {
|
|
27
|
+
const sessionId = getUserCookie(req.headers);
|
|
28
|
+
if (!sessionId) return null;
|
|
29
|
+
|
|
30
|
+
const { site } = getMagentoConfig();
|
|
31
|
+
const path = `/${encodeURIComponent(site)}/customer/section/load?sections=customer,carbono-customer`;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const res = await magentoFetch(path, {
|
|
35
|
+
headers: { Cookie: `${SESSION_COOKIE}=${sessionId}` },
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) return null;
|
|
38
|
+
|
|
39
|
+
const response = (await res.json()) as CustomerSectionLoad;
|
|
40
|
+
const carbono = response["carbono-customer"];
|
|
41
|
+
const customer = response.customer;
|
|
42
|
+
|
|
43
|
+
if (!carbono?.data_id || !customer) return null;
|
|
44
|
+
|
|
45
|
+
const { customerId, email } = carbono;
|
|
46
|
+
const { fullname, firstname } = customer;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
"@id": customerId,
|
|
50
|
+
email: email,
|
|
51
|
+
givenName: firstname,
|
|
52
|
+
...(firstname &&
|
|
53
|
+
fullname && {
|
|
54
|
+
familyName: fullname.replace(firstname, "").trim(),
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the visitor's saved wishlist via Magento's
|
|
3
|
+
* `/customer/section/load?sections=wishlist` endpoint, scoped by the
|
|
4
|
+
* PHPSESSID cookie.
|
|
5
|
+
*
|
|
6
|
+
* Ported from `deco-cx/apps/magento/loaders/wishlist.ts`. Returns null
|
|
7
|
+
* when the session cookie is absent or Magento reports no wishlist
|
|
8
|
+
* (e.g. a logged-in customer who never saved an item).
|
|
9
|
+
*/
|
|
10
|
+
import { getMagentoConfig, magentoFetch } from "../client";
|
|
11
|
+
import type { CustomerSectionLoad, Wishlist } from "../utils/client/types";
|
|
12
|
+
import { SESSION_COOKIE } from "../utils/constants";
|
|
13
|
+
import { getUserCookie } from "../utils/user";
|
|
14
|
+
|
|
15
|
+
export default async function wishlist(_props: unknown, req: Request): Promise<Wishlist | null> {
|
|
16
|
+
const sessionId = getUserCookie(req.headers);
|
|
17
|
+
if (!sessionId) return null;
|
|
18
|
+
|
|
19
|
+
const { site } = getMagentoConfig();
|
|
20
|
+
const path = `/${encodeURIComponent(site)}/customer/section/load?sections=wishlist`;
|
|
21
|
+
|
|
22
|
+
const res = await magentoFetch(path, {
|
|
23
|
+
headers: { Cookie: `${SESSION_COOKIE}=${sessionId}` },
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) return null;
|
|
26
|
+
|
|
27
|
+
const { wishlist } = (await res.json()) as CustomerSectionLoad;
|
|
28
|
+
return wishlist ?? null;
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magento middleware — currently a passthrough.
|
|
3
|
+
*
|
|
4
|
+
* The legacy deco-cx/apps middleware reconciled the cart id cookie
|
|
5
|
+
* after checkout (`changeCardIdAfterCheckout`) and seeded the
|
|
6
|
+
* `form_key` for anonymous sessions. Both flows touched response
|
|
7
|
+
* headers and `customer/section/load` endpoints — non-trivial port,
|
|
8
|
+
* deferred to a follow-up PR. Today the consumer site (granadobr-
|
|
9
|
+
* tanstack) handles cart reconciliation on the client.
|
|
10
|
+
*
|
|
11
|
+
* Shape matches `@decocms/apps-commerce/app-types` so it can be
|
|
12
|
+
* plugged into the autoconfig pipeline once magento is registered
|
|
13
|
+
* there.
|
|
14
|
+
*/
|
|
15
|
+
import type { AppMiddleware } from "@decocms/apps-commerce/app-types";
|
|
16
|
+
|
|
17
|
+
export const magentoMiddleware: AppMiddleware = async (_request, next) => {
|
|
18
|
+
return next();
|
|
19
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the Magento app — kept minimal in this initial port.
|
|
3
|
+
* Loader/action ports should extend this file rather than duplicating
|
|
4
|
+
* inline types.
|
|
5
|
+
*/
|
|
6
|
+
import type { MagentoFeatures } from "./client";
|
|
7
|
+
|
|
8
|
+
export interface MagentoCart {
|
|
9
|
+
id: string | null;
|
|
10
|
+
items: MagentoCartItem[];
|
|
11
|
+
totals?: {
|
|
12
|
+
subtotal?: number;
|
|
13
|
+
grand_total?: number;
|
|
14
|
+
discount_amount?: number;
|
|
15
|
+
shipping_amount?: number;
|
|
16
|
+
};
|
|
17
|
+
coupon_code?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MagentoCartItem {
|
|
21
|
+
item_id: number;
|
|
22
|
+
sku: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
qty: number;
|
|
25
|
+
price?: number;
|
|
26
|
+
row_total?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type Features = MagentoFeatures;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Magento newsletter subscription response shape — what
|
|
33
|
+
* `actions/newsletter/subscribe` returns to the storefront.
|
|
34
|
+
*/
|
|
35
|
+
export interface NewsletterData {
|
|
36
|
+
success: boolean;
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Stock-alert mutation response from Magento's GraphQL endpoint —
|
|
42
|
+
* `actions/product/stockAlert` returns this (or `{ error }`).
|
|
43
|
+
*/
|
|
44
|
+
export interface ProductStockAlertResponse {
|
|
45
|
+
data?: {
|
|
46
|
+
productStockAlert: {
|
|
47
|
+
message: string;
|
|
48
|
+
status: boolean;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache-time-control helpers — verbatim port of
|
|
3
|
+
* `deco-cx/apps/magento/utils/cacheTimeControl.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Magento product loaders pass the request URL through
|
|
6
|
+
* `filterSearchParamsFromURL` before using it as a cache key, so that
|
|
7
|
+
* tracking parameters (utm_*, gclid, fbclid, etc.) don't fragment the
|
|
8
|
+
* cache across thousands of variants for what is conceptually the
|
|
9
|
+
* same page.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_CACHE_MAX_AGE = 3600; // 1h
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pattern list (each entry is a regex source) of URL search-param
|
|
16
|
+
* keys that get stripped before a Magento URL goes into the cache key
|
|
17
|
+
* or downstream search-criteria. Mirrors prod byte-for-byte — the
|
|
18
|
+
* `.*` suffixes intentionally match `utm_source`, `utm_medium`, etc.
|
|
19
|
+
*/
|
|
20
|
+
export const SEARCH_PARAMS_TO_IGNORE = [
|
|
21
|
+
"gclid",
|
|
22
|
+
"gbraid",
|
|
23
|
+
"gdftrk",
|
|
24
|
+
"_ga",
|
|
25
|
+
"mc_.*",
|
|
26
|
+
"trk_.*",
|
|
27
|
+
"utm_.*",
|
|
28
|
+
"sc_.*",
|
|
29
|
+
"dm_i",
|
|
30
|
+
"_ke",
|
|
31
|
+
"fbclid",
|
|
32
|
+
"qitc",
|
|
33
|
+
"queryID",
|
|
34
|
+
"indexName",
|
|
35
|
+
"objectID",
|
|
36
|
+
"utm_source",
|
|
37
|
+
"utm_medium",
|
|
38
|
+
"utm_campaign",
|
|
39
|
+
"gad_.*",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Filter an array of [key, value] pairs, dropping any whose key matches
|
|
44
|
+
* one of the `SEARCH_PARAMS_TO_IGNORE` regex patterns. Pure function so
|
|
45
|
+
* callers can sort/transform after.
|
|
46
|
+
*/
|
|
47
|
+
export const filterSearchParams = (params: [string, string][]): [string, string][] => {
|
|
48
|
+
return params.filter(
|
|
49
|
+
([key]) => !SEARCH_PARAMS_TO_IGNORE.find((matchKey) => new RegExp(matchKey).test(key)),
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Strip the tracking params from a URL (or URL string) and return the
|
|
55
|
+
* cleaned `href`. Used by Magento PDP/PLP loaders as the cache key
|
|
56
|
+
* basis so cache lookups don't depend on the visitor's referer chain.
|
|
57
|
+
*/
|
|
58
|
+
export const filterSearchParamsFromURL = (defaultURL: string | URL): string => {
|
|
59
|
+
const url = new URL(defaultURL);
|
|
60
|
+
const paramsArray = Array.from(url.searchParams.entries());
|
|
61
|
+
const filteredParams = filterSearchParams(paramsArray);
|
|
62
|
+
|
|
63
|
+
url.search = "";
|
|
64
|
+
for (const [key, value] of filteredParams) {
|
|
65
|
+
url.searchParams.append(key, value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return url.href;
|
|
69
|
+
};
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magento REST API response shapes — the subset of types from
|
|
3
|
+
* `deco-cx/apps/magento/utils/client/types.ts` that the port has
|
|
4
|
+
* reached so far. Extended as more loaders/actions land.
|
|
5
|
+
*
|
|
6
|
+
* Keep field names **exactly** as Magento returns them (mostly
|
|
7
|
+
* snake_case, occasional camelCase from carbono-customer). Consumer
|
|
8
|
+
* sites already render against these shapes — any rename is a
|
|
9
|
+
* breaking change at the storefront, not just the API boundary.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Customer / user section payloads (added by the user+wishlist port)
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `customer` slice of `/customer/section/load?sections=customer,…`.
|
|
18
|
+
* Magento returns this on every authenticated section call.
|
|
19
|
+
*/
|
|
20
|
+
export interface Customer {
|
|
21
|
+
data_id: number;
|
|
22
|
+
fullname?: string;
|
|
23
|
+
firstname?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `carbono-customer` slice. Granado-specific overlay that mirrors the
|
|
28
|
+
* `customer` slice plus a website/store id pair and a normalized email.
|
|
29
|
+
* Other magento sites that don't run the Carbono module will get this
|
|
30
|
+
* absent; loaders/user.ts checks for it before mapping to a Person.
|
|
31
|
+
*/
|
|
32
|
+
export interface CarbonoCustomer {
|
|
33
|
+
websiteId?: string;
|
|
34
|
+
email?: string;
|
|
35
|
+
customerId?: string;
|
|
36
|
+
data_id: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `cart` slice of the customer section bundle — minimal projection of
|
|
41
|
+
* the cart that the minicart island renders before the full cart loader
|
|
42
|
+
* has resolved. Not the same as the full Cart payload from
|
|
43
|
+
* `/V1/carts/:cartId` (which lives in MagentoCart in types.ts).
|
|
44
|
+
*/
|
|
45
|
+
export interface CartUser {
|
|
46
|
+
summary_count: number;
|
|
47
|
+
subtotalAmount: number | null;
|
|
48
|
+
subtotal: string;
|
|
49
|
+
possible_onepage_checkout: boolean;
|
|
50
|
+
items: [];
|
|
51
|
+
isGuestCheckoutAllowed: boolean;
|
|
52
|
+
website_id: string;
|
|
53
|
+
storeId: string;
|
|
54
|
+
adyen_payment_methods: unknown[];
|
|
55
|
+
extra_actions: string;
|
|
56
|
+
cart_empty_message: string;
|
|
57
|
+
subtotal_incl_tax: string;
|
|
58
|
+
subtotal_excl_tax: string;
|
|
59
|
+
mpFSBCartTotal: unknown | null;
|
|
60
|
+
data_id: number;
|
|
61
|
+
minicart_improvements: MinicartImprovements;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface MinicartImprovements {
|
|
65
|
+
coupon_code: string | null;
|
|
66
|
+
country_id: string;
|
|
67
|
+
api_base_url: string;
|
|
68
|
+
is_logged_in: boolean;
|
|
69
|
+
quote_id: string;
|
|
70
|
+
base_url: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Bundle shape returned by
|
|
75
|
+
* `GET /:site/customer/section/load?sections=customer,carbono-customer,wishlist,…`.
|
|
76
|
+
* Keys are optional because the caller picks which sections to request.
|
|
77
|
+
*/
|
|
78
|
+
export interface CustomerSectionLoad {
|
|
79
|
+
customer?: Customer;
|
|
80
|
+
"carbono-customer"?: CarbonoCustomer;
|
|
81
|
+
cart?: CartUser;
|
|
82
|
+
wishlist?: Wishlist;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Wishlist payloads
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
export interface Wishlist {
|
|
90
|
+
counter: string;
|
|
91
|
+
items: WishlistItem[];
|
|
92
|
+
counter_number: number;
|
|
93
|
+
data_id: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface WishlistItem {
|
|
97
|
+
image: WishlistItemImage;
|
|
98
|
+
product_sku: string;
|
|
99
|
+
product_id: string;
|
|
100
|
+
product_url: string;
|
|
101
|
+
product_name: string;
|
|
102
|
+
product_price: string;
|
|
103
|
+
product_is_saleable_and_visible: boolean;
|
|
104
|
+
product_has_required_options: boolean;
|
|
105
|
+
add_to_cart_params: string;
|
|
106
|
+
delete_item_params: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface WishlistItemImage {
|
|
110
|
+
template: string;
|
|
111
|
+
src: string;
|
|
112
|
+
width: number;
|
|
113
|
+
height: number;
|
|
114
|
+
alt: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Shared attribute/category shapes (added by the transform port)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
export interface CustomAttribute {
|
|
122
|
+
attribute_code: string;
|
|
123
|
+
value: string | string[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface CategoryLink {
|
|
127
|
+
position: number;
|
|
128
|
+
category_id: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface MagentoCategory {
|
|
132
|
+
id: number;
|
|
133
|
+
parent_id: number;
|
|
134
|
+
name: string;
|
|
135
|
+
is_active: boolean;
|
|
136
|
+
position: number;
|
|
137
|
+
level: number;
|
|
138
|
+
children: string;
|
|
139
|
+
created_at: string;
|
|
140
|
+
updated_at: string;
|
|
141
|
+
path: string;
|
|
142
|
+
include_in_menu: boolean;
|
|
143
|
+
custom_attributes: CustomAttribute[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Product detail shapes (used by PDP / PLP / list loaders)
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
export interface MagentoPriceInfo {
|
|
151
|
+
final_price: number;
|
|
152
|
+
max_price: number;
|
|
153
|
+
max_regular_price: number;
|
|
154
|
+
minimal_regular_price: number;
|
|
155
|
+
special_price: number | null;
|
|
156
|
+
minimal_price: number;
|
|
157
|
+
regular_price: number;
|
|
158
|
+
formatted_prices: {
|
|
159
|
+
final_price: string;
|
|
160
|
+
max_price: string;
|
|
161
|
+
minimal_price: string;
|
|
162
|
+
max_regular_price: string;
|
|
163
|
+
minimal_regular_price: string | null;
|
|
164
|
+
special_price: string | null;
|
|
165
|
+
regular_price: string;
|
|
166
|
+
};
|
|
167
|
+
extension_attributes: {
|
|
168
|
+
msrp: {
|
|
169
|
+
msrp_price: string;
|
|
170
|
+
is_applicable: string;
|
|
171
|
+
is_shown_price_on_gesture: string;
|
|
172
|
+
msrp_message: string;
|
|
173
|
+
explanation_message: string;
|
|
174
|
+
};
|
|
175
|
+
tax_adjustments: {
|
|
176
|
+
final_price: number;
|
|
177
|
+
max_price: number;
|
|
178
|
+
max_regular_price: number;
|
|
179
|
+
minimal_regular_price: number;
|
|
180
|
+
special_price: number;
|
|
181
|
+
minimal_price: number;
|
|
182
|
+
regular_price: number;
|
|
183
|
+
formatted_prices: {
|
|
184
|
+
final_price: string;
|
|
185
|
+
max_price: string;
|
|
186
|
+
minimal_price: string;
|
|
187
|
+
max_regular_price: string;
|
|
188
|
+
minimal_regular_price: string | null;
|
|
189
|
+
special_price: string;
|
|
190
|
+
regular_price: string;
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
weee_attributes: unknown[];
|
|
194
|
+
weee_adjustment: string;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface MagentoStock {
|
|
199
|
+
item_id: number;
|
|
200
|
+
product_id: number;
|
|
201
|
+
stock_id: number;
|
|
202
|
+
qty?: number;
|
|
203
|
+
is_in_stock?: boolean;
|
|
204
|
+
is_qty_decimal?: boolean;
|
|
205
|
+
show_default_notification_message?: boolean;
|
|
206
|
+
use_config_min_qty?: boolean;
|
|
207
|
+
min_qty?: number;
|
|
208
|
+
use_config_min_sale_qty?: boolean;
|
|
209
|
+
min_sale_qty?: number;
|
|
210
|
+
use_config_max_sale_qty?: boolean;
|
|
211
|
+
max_sale_qty?: number;
|
|
212
|
+
use_config_backorders?: boolean;
|
|
213
|
+
backorders?: number;
|
|
214
|
+
use_config_notify_stock_qty?: boolean;
|
|
215
|
+
notify_stock_qty?: number;
|
|
216
|
+
use_config_qty_increments?: boolean;
|
|
217
|
+
qty_increments?: number;
|
|
218
|
+
use_config_enable_qty_inc?: boolean;
|
|
219
|
+
enable_qty_increments?: boolean;
|
|
220
|
+
use_config_manage_stock?: boolean;
|
|
221
|
+
manage_stock?: boolean;
|
|
222
|
+
low_stock_date?: string | null;
|
|
223
|
+
is_decimal_divided?: boolean;
|
|
224
|
+
stock_status_changed_auto?: number;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface MagentoImage {
|
|
228
|
+
url: string;
|
|
229
|
+
code: string;
|
|
230
|
+
height: number;
|
|
231
|
+
width: number;
|
|
232
|
+
label: string;
|
|
233
|
+
resized_width: number;
|
|
234
|
+
resized_height: number;
|
|
235
|
+
disabled: boolean;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface MediaEntry {
|
|
239
|
+
id: number;
|
|
240
|
+
media_type: string;
|
|
241
|
+
label: string | null;
|
|
242
|
+
position: number;
|
|
243
|
+
disabled: boolean;
|
|
244
|
+
types: string[];
|
|
245
|
+
file: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface MagentoProduct {
|
|
249
|
+
id: number;
|
|
250
|
+
sku: string;
|
|
251
|
+
name: string;
|
|
252
|
+
price: number;
|
|
253
|
+
status: number;
|
|
254
|
+
visibility: number;
|
|
255
|
+
type_id: string;
|
|
256
|
+
created_at: string;
|
|
257
|
+
updated_at: string;
|
|
258
|
+
weight: number;
|
|
259
|
+
url: string;
|
|
260
|
+
extension_attributes: {
|
|
261
|
+
website_ids?: number[];
|
|
262
|
+
category_links: CategoryLink[];
|
|
263
|
+
stock_item?: MagentoStock;
|
|
264
|
+
};
|
|
265
|
+
custom_attributes: CustomAttribute[];
|
|
266
|
+
price_info?: MagentoPriceInfo;
|
|
267
|
+
currency_code?: string;
|
|
268
|
+
images?: MagentoImage[];
|
|
269
|
+
media_gallery_entries?: MediaEntry[];
|
|
270
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magento constants — ported verbatim from
|
|
3
|
+
* `deco-cx/apps/magento/utils/constants.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Keep this file boring and append-only. Magento's REST and GraphQL
|
|
6
|
+
* payloads rely on these identifiers as-is (URL_KEY for PDP slug
|
|
7
|
+
* matching, GRAND_TOTAL/SUBTOTAL/… for the totals composition that
|
|
8
|
+
* cart.ts pulls, IN_STOCK/OUT_OF_STOCK for schema.org availability).
|
|
9
|
+
* Mutating any of these values would silently break consumer sites
|
|
10
|
+
* that already render against them.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { FiltersGraphQL } from "../client";
|
|
14
|
+
|
|
15
|
+
export const URL_KEY = "url_key";
|
|
16
|
+
|
|
17
|
+
// Schema.org availability mapping (used by utils/transform.ts to
|
|
18
|
+
// produce Offer.availability — kept here so transform doesn't import
|
|
19
|
+
// schema.org strings as magic literals).
|
|
20
|
+
export const IN_STOCK = "https://schema.org/InStock";
|
|
21
|
+
export const OUT_OF_STOCK = "https://schema.org/OutOfStock";
|
|
22
|
+
|
|
23
|
+
// Rating bounds used by `utils/transform.ts` (and the review/rating
|
|
24
|
+
// loaders that follow) when mapping Magento's integer-rating scale
|
|
25
|
+
// into schema.org `AggregateRating`'s 1–5 range.
|
|
26
|
+
export const MAX_RATING_VALUE = 5;
|
|
27
|
+
export const MIN_RATING_VALUE = 1;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default filter mapping consumed by `utils/graphql.ts:filtersFromUrlGraphQL`.
|
|
31
|
+
* Each entry pairs a Magento attribute slug with the comparison operator
|
|
32
|
+
* the storefront's URL filters use. Sites can extend this via the
|
|
33
|
+
* `customFilters` prop on PLP/list loaders without forking the array.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_GRAPHQL_FILTERS: FiltersGraphQL[] = [
|
|
36
|
+
{ value: "activity", type: "EQUAL" },
|
|
37
|
+
{ value: "category_gear", type: "EQUAL" },
|
|
38
|
+
{ value: "category_id", type: "EQUAL" },
|
|
39
|
+
{ value: "category_uid", type: "EQUAL" },
|
|
40
|
+
{ value: "category_url_path", type: "EQUAL" },
|
|
41
|
+
{ value: "climate", type: "EQUAL" },
|
|
42
|
+
{ value: "collar", type: "EQUAL" },
|
|
43
|
+
{ value: "color", type: "EQUAL" },
|
|
44
|
+
{ value: "description", type: "MATCH" },
|
|
45
|
+
{ value: "eco_collection", type: "EQUAL" },
|
|
46
|
+
{ value: "erin_recommends", type: "EQUAL" },
|
|
47
|
+
{ value: "features_bags", type: "EQUAL" },
|
|
48
|
+
{ value: "format", type: "EQUAL" },
|
|
49
|
+
{ value: "gender", type: "EQUAL" },
|
|
50
|
+
{ value: "material", type: "EQUAL" },
|
|
51
|
+
{ value: "name", type: "MATCH" },
|
|
52
|
+
{ value: "new", type: "EQUAL" },
|
|
53
|
+
{ value: "pattern", type: "EQUAL" },
|
|
54
|
+
{ value: "performance_fabric", type: "EQUAL" },
|
|
55
|
+
{ value: "price", type: "RANGE" },
|
|
56
|
+
{ value: "purpose", type: "EQUAL" },
|
|
57
|
+
{ value: "sale", type: "EQUAL" },
|
|
58
|
+
{ value: "short_description", type: "MATCH" },
|
|
59
|
+
{ value: "size", type: "EQUAL" },
|
|
60
|
+
{ value: "sku", type: "EQUAL" },
|
|
61
|
+
{ value: "sleeve", type: "EQUAL" },
|
|
62
|
+
{ value: "strap_bags", type: "EQUAL" },
|
|
63
|
+
{ value: "style_bags", type: "EQUAL" },
|
|
64
|
+
{ value: "style_bottom", type: "EQUAL" },
|
|
65
|
+
{ value: "style_general", type: "EQUAL" },
|
|
66
|
+
{ value: "url_key", type: "EQUAL" },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Query-string keys that should be stripped before forwarding a request
|
|
71
|
+
* URL to Magento (e.g. when computing a cache key or building a
|
|
72
|
+
* paginated request). Mirrors the Fresh-era REMOVABLE_URL_SEARCHPARAMS.
|
|
73
|
+
*/
|
|
74
|
+
export const REMOVABLE_URL_SEARCHPARAMS = ["p", "product_list_order"];
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Cart totals composition — field names sent to Magento's
|
|
78
|
+
// /V1/carts/:cartId/totals?fields=… endpoint. The cart loader joins
|
|
79
|
+
// these into the `fields` query param to keep the response narrow.
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
export const GRAND_TOTAL = "grand_total";
|
|
83
|
+
export const SUBTOTAL = "subtotal";
|
|
84
|
+
export const DISCOUNT_AMOUNT = "discount_amount";
|
|
85
|
+
export const BASE_DISCOUNT_AMOUNT = "base_discount_amount";
|
|
86
|
+
export const SHIPPING_AMOUNT = "shipping_amount";
|
|
87
|
+
export const BASE_SHIPPING_AMOUNT = "base_shipping_amount";
|
|
88
|
+
export const SHIPPING_DISCOUNT_AMOUNT = "shipping_discount_amount";
|
|
89
|
+
export const COUPON_CODE = "coupon_code";
|
|
90
|
+
export const BASE_CURRENCY_CODE = "base_currency_code";
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Cookie names — single source of truth so loaders/actions/middleware
|
|
94
|
+
// don't drift on string literals (Magento is case-sensitive about
|
|
95
|
+
// these and a typo produces a silent anonymous-session bug).
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export const SESSION_COOKIE = "PHPSESSID";
|
|
99
|
+
export const CUSTOMER_COOKIE = "dataservices_customer_id";
|
|
100
|
+
export const CART_COOKIE = "dataservices_cart_id";
|
|
101
|
+
export const FORM_KEY_COOKIE = "form_key";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magento GraphQL request input types used by the product loaders.
|
|
3
|
+
*
|
|
4
|
+
* Subset of `deco-cx/apps/magento/utils/clientGraphql/types.ts` that the
|
|
5
|
+
* port has reached so far — extended as more loaders land. These mirror
|
|
6
|
+
* Magento's GraphQL schema for product search/sort/filter so the
|
|
7
|
+
* storefront's URL params translate cleanly into GraphQL variables.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Magento ProductAttributeFilterInput shape — keys are Magento
|
|
12
|
+
* attribute codes; values are one of the three filter-type unions.
|
|
13
|
+
* Maps to: `filter: ProductAttributeFilterInput!`
|
|
14
|
+
*/
|
|
15
|
+
export interface ProductFilterInput {
|
|
16
|
+
[key: string]: FilterEqualTypeInput | FilterMatchTypeInput | FilterRangeTypeInput;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Magento `FilterEqualTypeInput` — for exact-match attributes (sku,
|
|
21
|
+
* sale, color, size, etc.). `in` lets you OR multiple values; the
|
|
22
|
+
* single-value `eq` form is also accepted by the Magento schema and is
|
|
23
|
+
* the shape `transformFilterValueGraphQL` emits.
|
|
24
|
+
*/
|
|
25
|
+
export interface FilterEqualTypeInput {
|
|
26
|
+
in?: string[];
|
|
27
|
+
eq?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Magento `FilterMatchTypeInput` — substring-match attributes (name,
|
|
32
|
+
* description, short_description).
|
|
33
|
+
*/
|
|
34
|
+
export interface FilterMatchTypeInput {
|
|
35
|
+
match: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Magento `FilterRangeTypeInput` — numeric ranges (price). Both bounds
|
|
40
|
+
* are strings in the schema; ranges in URL params come as `from_to`.
|
|
41
|
+
*/
|
|
42
|
+
export interface FilterRangeTypeInput {
|
|
43
|
+
from: string;
|
|
44
|
+
to: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Magento `ProductAttributeSortInput` — one entry per sortable
|
|
49
|
+
* attribute keyed by attribute code, ordered ASC or DESC.
|
|
50
|
+
*/
|
|
51
|
+
export interface ProductSortInput {
|
|
52
|
+
[key: string]: "ASC" | "DESC";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Built-in sort options surfaced in the CMS admin. Custom options can
|
|
57
|
+
* be added by sites via `CustomProductSortOption`.
|
|
58
|
+
*/
|
|
59
|
+
export interface DefaultProductSortOption {
|
|
60
|
+
value: "name" | "position" | "price" | "relevance";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CustomProductSortOption {
|
|
64
|
+
value: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Shared sort-prop shape used by PLP / list / relatedProducts loaders.
|
|
69
|
+
*/
|
|
70
|
+
export interface ProductSort {
|
|
71
|
+
/** @title Order by */
|
|
72
|
+
sortBy: DefaultProductSortOption | CustomProductSortOption;
|
|
73
|
+
/** @title Sequency */
|
|
74
|
+
order: "ASC" | "DESC";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Loader-supplied filter (vs URL-derived). The site can hard-code
|
|
79
|
+
* filters in the CMS section config and they layer on top of whatever
|
|
80
|
+
* the user picked from URL params.
|
|
81
|
+
*/
|
|
82
|
+
export interface FilterProps {
|
|
83
|
+
name: string;
|
|
84
|
+
type: FilterEqualTypeInput | FilterMatchTypeInput | FilterRangeTypeInput;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Custom-fields toggle used by `getCustomFields()` to decide which
|
|
89
|
+
* Magento product attributes to project from a query.
|
|
90
|
+
*/
|
|
91
|
+
export interface CustomFields {
|
|
92
|
+
/**
|
|
93
|
+
* @description Search for global custom fields defined in App settings
|
|
94
|
+
* @default false
|
|
95
|
+
*/
|
|
96
|
+
active: boolean;
|
|
97
|
+
/** @description Will override global custom fields defined in App settings */
|
|
98
|
+
overrideList?: string[];
|
|
99
|
+
}
|