@laioutr/app-shopware 0.17.0 → 0.19.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/README.md +3 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/server/composable/useGetProductParentId.js +12 -15
- package/dist/runtime/server/orchestr/auth/login-oauth.action.d.ts +10 -0
- package/dist/runtime/server/orchestr/auth/login-oauth.action.js +13 -0
- package/dist/runtime/server/orchestr/cart/add-item.action.js +42 -17
- package/dist/runtime/server/orchestr/cart/cart-items.link.js +2 -1
- package/dist/runtime/server/orchestr/cart-item/base.resolver.js +3 -3
- package/dist/runtime/server/orchestr/customer/get-current.action.d.ts +10 -0
- package/dist/runtime/server/orchestr/customer/get-current.action.js +22 -0
- package/dist/runtime/server/orchestr/menu/byAlias.template.js +1 -1
- package/dist/runtime/server/orchestr/product/reviews.link.js +12 -1
- package/dist/runtime/server/shopware-helper/cartErrors.d.ts +25 -1
- package/dist/runtime/server/shopware-helper/cartErrors.js +11 -0
- package/dist/runtime/server/shopware-helper/cartMapper.d.ts +24 -2
- package/dist/runtime/server/shopware-helper/cartMapper.js +27 -0
- package/dist/runtime/server/shopware-helper/system/getSystemEntities.js +5 -4
- package/dist/runtime/server/shopware-helper/useSeoResolver.js +5 -4
- package/package.json +19 -19
package/README.md
CHANGED
|
@@ -17,8 +17,9 @@ See [laioutr.com](https://laioutr.com) for more information about Laioutr.
|
|
|
17
17
|
|
|
18
18
|
- Products, categories, menus, reviews and search served through Laioutr's Orchestr layer, mapped
|
|
19
19
|
onto the canonical entity model — so storefront components stay backend-agnostic
|
|
20
|
-
- Cart as a first-class entity: add, update and remove items,
|
|
21
|
-
tokens that never reach
|
|
20
|
+
- Cart as a first-class entity: add, update and remove items, redeem discount codes, and read back
|
|
21
|
+
the shop's automatic cart discounts — with server-held Shopware context tokens that never reach
|
|
22
|
+
the browser
|
|
22
23
|
- Embedded checkout — the Shopware storefront's own checkout rendered in-page, with a same-origin
|
|
23
24
|
session handoff so the visitor is never bounced to another domain
|
|
24
25
|
- Auth bridge: a storefront login or logout inside the embedded checkout propagates back into the
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { CHECKOUT_ENDPOINT_PATH, ADOPT_SESSION_ENDPOINT_PATH, ORDER_HANDOFF_ENDP
|
|
|
4
4
|
import { registerLaioutrApp } from '@laioutr-core/kit';
|
|
5
5
|
|
|
6
6
|
const name = "@laioutr/app-shopware";
|
|
7
|
-
const version = "0.
|
|
7
|
+
const version = "0.19.0";
|
|
8
8
|
|
|
9
9
|
const module$1 = defineNuxtModule({
|
|
10
10
|
meta: {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useUserlandCache } from "#imports";
|
|
2
2
|
const useProductParentIdCache = () => useUserlandCache("shopware/product-parent-id");
|
|
3
3
|
const PRODUCT_PARENT_ID_CACHE_TTL = 60 * 60 * 24 * 7;
|
|
4
|
+
const entryOptions = { maxAge: PRODUCT_PARENT_ID_CACHE_TTL };
|
|
4
5
|
export const useGetProductParentId = (storefrontClient) => {
|
|
5
6
|
const cache = useProductParentIdCache();
|
|
6
7
|
return async (id) => {
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
9
|
-
return
|
|
8
|
+
const cached = await cache.readOne(id, entryOptions);
|
|
9
|
+
if (cached && !cached.absent) {
|
|
10
|
+
return cached.value;
|
|
10
11
|
}
|
|
11
12
|
try {
|
|
12
13
|
const response = await storefrontClient.invoke("readProduct post /product", {
|
|
@@ -17,7 +18,7 @@ export const useGetProductParentId = (storefrontClient) => {
|
|
|
17
18
|
});
|
|
18
19
|
const parentId = response.data.elements?.[0]?.parentId;
|
|
19
20
|
if (parentId) {
|
|
20
|
-
cache.
|
|
21
|
+
cache.writeOne(id, parentId, entryOptions);
|
|
21
22
|
return parentId;
|
|
22
23
|
}
|
|
23
24
|
return void 0;
|
|
@@ -27,15 +28,11 @@ export const useGetProductParentId = (storefrontClient) => {
|
|
|
27
28
|
};
|
|
28
29
|
};
|
|
29
30
|
export const cacheProductParentIds = (productIdsToParentIds) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
value: parentId
|
|
37
|
-
})),
|
|
38
|
-
{ ttl: PRODUCT_PARENT_ID_CACHE_TTL }
|
|
39
|
-
)
|
|
31
|
+
useProductParentIdCache().write(
|
|
32
|
+
productIdsToParentIds.map(([productId, parentId]) => ({
|
|
33
|
+
key: productId,
|
|
34
|
+
value: parentId,
|
|
35
|
+
options: entryOptions
|
|
36
|
+
}))
|
|
40
37
|
);
|
|
41
38
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send a visitor asking for their account to Shopware's own login page. Shopware authenticates
|
|
3
|
+
* with credentials rather than an OAuth handshake, so this answers the union's `account` arm and
|
|
4
|
+
* the storefront navigates there.
|
|
5
|
+
*
|
|
6
|
+
* `returnTo` is dropped: Shopware's login redirect takes one of its own route names, not a URL,
|
|
7
|
+
* so a laioutr path cannot travel through it.
|
|
8
|
+
*/
|
|
9
|
+
declare const _default: any;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "#imports";
|
|
2
|
+
import { AuthLoginOauthAction } from "@laioutr-core/canonical-types/ecommerce";
|
|
3
|
+
import { defineShopwareAction } from "../../middleware/defineShopware.js";
|
|
4
|
+
export default defineShopwareAction(AuthLoginOauthAction, async () => {
|
|
5
|
+
const { storefrontUrl } = useRuntimeConfig()["@laioutr/app-shopware"];
|
|
6
|
+
if (!storefrontUrl) {
|
|
7
|
+
throw new Error("storefrontUrl is not configured, so there is no Shopware login page to send the customer to");
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
type: "account",
|
|
11
|
+
link: { type: "url", href: new URL("/account/login", storefrontUrl).toString() }
|
|
12
|
+
};
|
|
13
|
+
});
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { CartAddItemsAction } from "@laioutr-core/canonical-types/ecommerce";
|
|
2
2
|
import { defineShopwareAction } from "../../middleware/defineShopware.js";
|
|
3
|
-
import { handleCartMutationErrors } from "../../shopware-helper/cartErrors.js";
|
|
3
|
+
import { handleCartMutationErrors, takeDiscountCodeErrors } from "../../shopware-helper/cartErrors.js";
|
|
4
4
|
import { persistContextToken } from "../../shopware-helper/persistContextToken.js";
|
|
5
5
|
export default defineShopwareAction(CartAddItemsAction, async ({ event, context, input }) => {
|
|
6
6
|
const { storefrontClient } = context;
|
|
7
|
+
let persistedToken;
|
|
8
|
+
const persistToken = async (token) => {
|
|
9
|
+
if (!token || token === persistedToken) return;
|
|
10
|
+
persistedToken = token;
|
|
11
|
+
await persistContextToken(event, token);
|
|
12
|
+
};
|
|
7
13
|
const products = input.filter((i) => i.type === "product");
|
|
8
|
-
const skuItems = input.filter((i) => i.type === "sku");
|
|
9
14
|
if (products.length > 0) {
|
|
10
15
|
const cart = await storefrontClient.invoke("addLineItem post /checkout/cart/line-item", {
|
|
11
16
|
body: {
|
|
@@ -23,24 +28,44 @@ export default defineShopwareAction(CartAddItemsAction, async ({ event, context,
|
|
|
23
28
|
}))
|
|
24
29
|
}
|
|
25
30
|
});
|
|
26
|
-
await
|
|
31
|
+
await persistToken(cart.data.token);
|
|
27
32
|
handleCartMutationErrors(cart.data.errors);
|
|
28
33
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
const redeemDiscountCode = async (code) => {
|
|
35
|
+
const cart = await storefrontClient.invoke("addLineItem post /checkout/cart/line-item", {
|
|
36
|
+
body: { items: [{ type: "promotion", referencedId: code }] }
|
|
37
|
+
});
|
|
38
|
+
await persistToken(cart.data.token);
|
|
39
|
+
const { rejections, rest } = takeDiscountCodeErrors(cart.data.errors);
|
|
40
|
+
handleCartMutationErrors(rest);
|
|
41
|
+
const applied = (cart.data.lineItems ?? []).some((li) => li.type === "promotion" && li.referencedId === code);
|
|
42
|
+
if (applied) return { status: "added", quantity: 1 };
|
|
43
|
+
const [rejection] = rejections;
|
|
44
|
+
const key = rejection?.messageKey ?? rejection?.key;
|
|
45
|
+
return {
|
|
46
|
+
status: "rejected",
|
|
47
|
+
reason: key === "promotion-not-found" || key === void 0 ? "not-found" : key,
|
|
48
|
+
reasonLabel: rejection?.message
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const items = [];
|
|
52
|
+
for (const item of input) {
|
|
53
|
+
if (item.type === "product") {
|
|
54
|
+
items.push({
|
|
32
55
|
status: "added",
|
|
33
|
-
productId:
|
|
34
|
-
variantId:
|
|
35
|
-
quantity:
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
productId: item.productId,
|
|
57
|
+
variantId: item.variantId,
|
|
58
|
+
quantity: item.quantity
|
|
59
|
+
});
|
|
60
|
+
} else if (item.type === "discount-code") {
|
|
61
|
+
items.push(await redeemDiscountCode(item.code));
|
|
62
|
+
} else {
|
|
63
|
+
items.push({
|
|
40
64
|
status: "rejected",
|
|
41
|
-
sku: item.sku,
|
|
65
|
+
...item.type === "sku" ? { sku: item.sku } : {},
|
|
42
66
|
reason: "not-supported"
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { items };
|
|
46
71
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { CartItemsLink } from "@laioutr-core/canonical-types/ecommerce";
|
|
2
2
|
import { cartFragmentToken } from "../../const/passthroughTokens.js";
|
|
3
3
|
import { defineShopwareLink } from "../../middleware/defineShopware.js";
|
|
4
|
+
import { isSupportedCartLineItem } from "../../shopware-helper/cartMapper.js";
|
|
4
5
|
import { getCart } from "../../shopware-helper/getCart.js";
|
|
5
6
|
export default defineShopwareLink(CartItemsLink, async ({ context, passthrough }) => {
|
|
6
7
|
const cart = passthrough.get(cartFragmentToken) ?? await getCart(context.storefrontClient);
|
|
7
8
|
passthrough.set(cartFragmentToken, cart);
|
|
8
|
-
const targetIds = (cart.lineItems ?? []).filter(
|
|
9
|
+
const targetIds = (cart.lineItems ?? []).filter(isSupportedCartLineItem).map((li) => li.id);
|
|
9
10
|
return { links: [{ sourceId: cart.token ?? "", targetIds }] };
|
|
10
11
|
});
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "@laioutr-core/canonical-types/entity/cart-item";
|
|
8
8
|
import { cartFragmentToken } from "../../const/passthroughTokens.js";
|
|
9
9
|
import { defineShopwareComponentResolver } from "../../middleware/defineShopware.js";
|
|
10
|
-
import { mapCartItem } from "../../shopware-helper/cartMapper.js";
|
|
10
|
+
import { isSupportedCartLineItem, mapCartItem, mapDiscountItem } from "../../shopware-helper/cartMapper.js";
|
|
11
11
|
import { getCart } from "../../shopware-helper/getCart.js";
|
|
12
12
|
export default defineShopwareComponentResolver({
|
|
13
13
|
entityType: "CartItem",
|
|
@@ -17,8 +17,8 @@ export default defineShopwareComponentResolver({
|
|
|
17
17
|
const cart = passthrough.get(cartFragmentToken) ?? await getCart(context.storefrontClient);
|
|
18
18
|
passthrough.set(cartFragmentToken, cart);
|
|
19
19
|
const lineItemsById = new Map((cart.lineItems ?? []).map((li) => [li.id, li]));
|
|
20
|
-
const entities = entityIds.map((id) => lineItemsById.get(id)).filter((li) => !!li && li
|
|
21
|
-
const mapped = mapCartItem(li, context.swCurrency);
|
|
20
|
+
const entities = entityIds.map((id) => lineItemsById.get(id)).filter((li) => !!li && isSupportedCartLineItem(li)).map((li) => {
|
|
21
|
+
const mapped = li.type === "promotion" ? mapDiscountItem(li, context.swCurrency) : mapCartItem(li, context.swCurrency);
|
|
22
22
|
return $entity({
|
|
23
23
|
id: li.id,
|
|
24
24
|
base: () => mapped.base,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the customer behind the request's context token. Shopware answers `403` when the token
|
|
3
|
+
* belongs to a guest context, which is the signal callers use to tell a session from none.
|
|
4
|
+
*
|
|
5
|
+
* `addresses` stays empty: Shopware returns addresses only as an association, and each one needs
|
|
6
|
+
* its `countryId` resolved to the ISO code `MailingAddress` requires. Nothing reads them yet, so
|
|
7
|
+
* the lookup is not made — extend this handler before relying on the field.
|
|
8
|
+
*/
|
|
9
|
+
declare const _default: any;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CustomerGetCurrentAction, UnauthenticatedError } from "@laioutr-core/canonical-types/ecommerce";
|
|
2
|
+
import { defineShopwareAction } from "../../middleware/defineShopware.js";
|
|
3
|
+
export default defineShopwareAction(CustomerGetCurrentAction, async ({ context }) => {
|
|
4
|
+
let customer;
|
|
5
|
+
try {
|
|
6
|
+
customer = await context.storefrontClient.invoke("readCustomer post /account/customer", {});
|
|
7
|
+
} catch {
|
|
8
|
+
throw new UnauthenticatedError();
|
|
9
|
+
}
|
|
10
|
+
const firstName = customer.firstName ?? "";
|
|
11
|
+
const lastName = customer.lastName ?? "";
|
|
12
|
+
const displayName = `${firstName} ${lastName}`.trim();
|
|
13
|
+
return {
|
|
14
|
+
customer: {
|
|
15
|
+
id: customer.id,
|
|
16
|
+
email: customer.email,
|
|
17
|
+
displayName: displayName || (customer.email ?? ""),
|
|
18
|
+
person: { firstName, lastName, salutation: customer.salutation?.salutationKey, title: customer.title ?? void 0 },
|
|
19
|
+
addresses: []
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
});
|
|
@@ -2,7 +2,7 @@ import { MenuByAliasQuery } from "@laioutr-core/canonical-types/ecommerce";
|
|
|
2
2
|
import { defineShopwareQueryTemplateProvider } from "../../middleware/defineShopware.js";
|
|
3
3
|
export default defineShopwareQueryTemplateProvider({
|
|
4
4
|
for: MenuByAliasQuery,
|
|
5
|
-
run: async (
|
|
5
|
+
run: async () => [
|
|
6
6
|
{
|
|
7
7
|
input: {
|
|
8
8
|
alias: "main-navigation"
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { ProductReviewsLink } from "@laioutr-core/canonical-types/ecommerce";
|
|
2
2
|
import { currentProductIdsToken } from "../../const/passthroughTokens.js";
|
|
3
3
|
import { defineShopwareLink } from "../../middleware/defineShopware.js";
|
|
4
|
-
|
|
4
|
+
const REVIEW_SORTINGS = {
|
|
5
|
+
newest: { field: "createdAt", order: "DESC" },
|
|
6
|
+
oldest: { field: "createdAt", order: "ASC" },
|
|
7
|
+
"rating-high": { field: "points", order: "DESC" },
|
|
8
|
+
"rating-low": { field: "points", order: "ASC" }
|
|
9
|
+
};
|
|
10
|
+
export default defineShopwareLink(ProductReviewsLink, async ({ entityIds, context, pagination, sorting, filter, passthrough }) => {
|
|
5
11
|
const { storefrontClient } = context;
|
|
12
|
+
const sort = sorting ? REVIEW_SORTINGS[sorting] : void 0;
|
|
13
|
+
const points = Number(filter?.points);
|
|
14
|
+
const criteriaFilter = Number.isFinite(points) ? [{ type: "equals", field: "points", value: points }] : void 0;
|
|
6
15
|
const productToReviews = {};
|
|
7
16
|
await Promise.all(
|
|
8
17
|
entityIds.map(async (entityId) => {
|
|
@@ -11,6 +20,8 @@ export default defineShopwareLink(ProductReviewsLink, async ({ entityIds, contex
|
|
|
11
20
|
body: {
|
|
12
21
|
page: pagination.page,
|
|
13
22
|
limit: pagination.limit,
|
|
23
|
+
...sort ? { sort: [sort] } : {},
|
|
24
|
+
...criteriaFilter ? { filter: criteriaFilter } : {},
|
|
14
25
|
includes: {
|
|
15
26
|
product_review: ["id"]
|
|
16
27
|
}
|
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
import { Schemas } from '../types/storeApiTypes.js';
|
|
2
|
+
export type CartErrorEntry = {
|
|
3
|
+
key?: string;
|
|
4
|
+
level?: number;
|
|
5
|
+
message?: string;
|
|
6
|
+
messageKey?: string;
|
|
7
|
+
};
|
|
8
|
+
type CartErrors = Schemas['Cart']['errors'] | CartErrorEntry[];
|
|
9
|
+
/**
|
|
10
|
+
* Split the cart errors that describe a submitted discount code off from the rest.
|
|
11
|
+
*
|
|
12
|
+
* `CartAddItemsAction` reports a code it could not redeem as a per-item rejection instead of
|
|
13
|
+
* failing the whole call, so these are handed back to the caller rather than thrown. Shopware
|
|
14
|
+
* levels `promotion-not-found` at 20 — the threshold `handleCartMutationErrors` throws on —
|
|
15
|
+
* while setting `blockOrder(): false` on it.
|
|
16
|
+
*
|
|
17
|
+
* Matching is by substring because the keys are not uniformly prefixed: a rejected code is
|
|
18
|
+
* `promotion-not-found`, an ineligible one carries its reason as a suffix
|
|
19
|
+
* (`promotion-not-eligible-<reason>`), and the automatic-promotion key leads with `auto-`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const takeDiscountCodeErrors: (errors: CartErrors) => {
|
|
22
|
+
rejections: CartErrorEntry[];
|
|
23
|
+
rest: CartErrorEntry[];
|
|
24
|
+
};
|
|
2
25
|
/**
|
|
3
26
|
* Inspect the `errors` of a Shopware cart mutation response.
|
|
4
27
|
*
|
|
@@ -8,4 +31,5 @@ import { Schemas } from '../types/storeApiTypes.js';
|
|
|
8
31
|
* Product* errors is deferred — Shopware's CartError shape carries no reliable
|
|
9
32
|
* variant reference.)
|
|
10
33
|
*/
|
|
11
|
-
export declare const handleCartMutationErrors: (errors:
|
|
34
|
+
export declare const handleCartMutationErrors: (errors: CartErrors) => void;
|
|
35
|
+
export {};
|
|
@@ -5,6 +5,17 @@ const normalizeCartErrors = (errors) => {
|
|
|
5
5
|
if (Array.isArray(errors)) return errors;
|
|
6
6
|
return Object.values(errors);
|
|
7
7
|
};
|
|
8
|
+
const autoPromotionNotFoundKey = "auto-promotion-not-found";
|
|
9
|
+
export const takeDiscountCodeErrors = (errors) => {
|
|
10
|
+
const rejections = [];
|
|
11
|
+
const rest = [];
|
|
12
|
+
for (const error of normalizeCartErrors(errors)) {
|
|
13
|
+
const key = error.messageKey ?? error.key ?? "";
|
|
14
|
+
if (key.includes("promotion") && key !== autoPromotionNotFoundKey) rejections.push(error);
|
|
15
|
+
else rest.push(error);
|
|
16
|
+
}
|
|
17
|
+
return { rejections, rest };
|
|
18
|
+
};
|
|
8
19
|
export const handleCartMutationErrors = (errors) => {
|
|
9
20
|
for (const error of normalizeCartErrors(errors)) {
|
|
10
21
|
const level = error.level ?? 0;
|
|
@@ -4,11 +4,33 @@ import { EntityComponentType } from '@laioutr-core/core-types/orchestr';
|
|
|
4
4
|
import { Schemas } from '../types/storeApiTypes.js';
|
|
5
5
|
/** Map a Shopware cart's aggregate price into the canonical nested `CartCost`. */
|
|
6
6
|
export declare const mapCartCost: (cart: Schemas["Cart"], currency: string) => EntityComponentType<typeof CartCost>;
|
|
7
|
-
|
|
8
|
-
export declare const mapCartItem: (lineItem: Schemas["LineItem"], currency: string) => {
|
|
7
|
+
type MappedCartItem = {
|
|
9
8
|
base: EntityComponentType<typeof CartItemBase>;
|
|
10
9
|
cost: EntityComponentType<typeof CartItemCost>;
|
|
11
10
|
availability: EntityComponentType<typeof CartItemAvailability>;
|
|
12
11
|
quantityRule: EntityComponentType<typeof CartItemQuantityRule>;
|
|
13
12
|
productData: EntityComponentType<typeof CartItemProductData>;
|
|
14
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Line-item types this app surfaces as canonical `CartItem`s.
|
|
16
|
+
*
|
|
17
|
+
* Shopware carries promotions as line items of type `promotion` — both codes the customer
|
|
18
|
+
* entered and cart-wide discounts the shop applies automatically. Their (negative) price is
|
|
19
|
+
* already part of `cart.price.positionPrice`, so dropping them leaves a cart whose items add
|
|
20
|
+
* up to more than its subtotal with nothing to explain the gap.
|
|
21
|
+
*/
|
|
22
|
+
export declare const isSupportedCartLineItem: (lineItem: Schemas["LineItem"]) => boolean;
|
|
23
|
+
/** Map a Shopware product line item into the canonical `CartItem` component values. */
|
|
24
|
+
export declare const mapCartItem: (lineItem: Schemas["LineItem"], currency: string) => MappedCartItem;
|
|
25
|
+
/**
|
|
26
|
+
* Map a Shopware promotion line item into the canonical `CartItem` component values.
|
|
27
|
+
*
|
|
28
|
+
* The amount is negative: Shopware prices a promotion as a discount off the cart, and the
|
|
29
|
+
* canonical `Money` carries the sign.
|
|
30
|
+
*
|
|
31
|
+
* `discount-code` is the closest canonical type, but the code is only there for a promotion
|
|
32
|
+
* the customer redeemed — an automatic cart discount reaches us with an empty `code` and an
|
|
33
|
+
* empty `referencedId`, so `code` is left unset rather than blank.
|
|
34
|
+
*/
|
|
35
|
+
export declare const mapDiscountItem: (lineItem: Schemas["LineItem"], currency: string) => MappedCartItem;
|
|
36
|
+
export {};
|
|
@@ -30,6 +30,7 @@ export const mapCartCost = (cart, currency) => {
|
|
|
30
30
|
...taxes.length > 0 ? { taxes } : {}
|
|
31
31
|
};
|
|
32
32
|
};
|
|
33
|
+
export const isSupportedCartLineItem = (lineItem) => lineItem.type === "product" || lineItem.type === "promotion";
|
|
33
34
|
export const mapCartItem = (lineItem, currency) => {
|
|
34
35
|
const money = (value) => Money.fromDecimal(value, currency);
|
|
35
36
|
const price = lineItem.price;
|
|
@@ -85,3 +86,29 @@ export const mapCartItem = (lineItem, currency) => {
|
|
|
85
86
|
productData: void 0
|
|
86
87
|
};
|
|
87
88
|
};
|
|
89
|
+
export const mapDiscountItem = (lineItem, currency) => {
|
|
90
|
+
const money = (value) => Money.fromDecimal(value, currency);
|
|
91
|
+
const price = lineItem.price;
|
|
92
|
+
const total = price?.totalPrice ?? 0;
|
|
93
|
+
const payload = lineItem.payload ?? {};
|
|
94
|
+
const code = payload.code || lineItem.referencedId || void 0;
|
|
95
|
+
return {
|
|
96
|
+
base: {
|
|
97
|
+
type: "discount-code",
|
|
98
|
+
quantity: lineItem.quantity,
|
|
99
|
+
// Shopware labels the item with the promotion's translated name.
|
|
100
|
+
title: lineItem.label ?? "",
|
|
101
|
+
code
|
|
102
|
+
},
|
|
103
|
+
cost: {
|
|
104
|
+
single: money(price?.unitPrice ?? total),
|
|
105
|
+
subtotal: money(total),
|
|
106
|
+
total: money(total)
|
|
107
|
+
},
|
|
108
|
+
// A promotion in the cart is applied by definition, and its quantity is Shopware's to
|
|
109
|
+
// decide — the canonical components are required, so they state exactly that.
|
|
110
|
+
availability: { status: "inStock", quantity: lineItem.quantity },
|
|
111
|
+
quantityRule: { min: 1, max: 1, increment: 1, canChange: false },
|
|
112
|
+
productData: void 0
|
|
113
|
+
};
|
|
114
|
+
};
|
|
@@ -34,14 +34,15 @@ export const getSystemEntities = async (client) => {
|
|
|
34
34
|
};
|
|
35
35
|
};
|
|
36
36
|
const SYSTEM_ENTITIES_TTL = 60 * 60 * 24;
|
|
37
|
+
const entryOptions = { maxAge: SYSTEM_ENTITIES_TTL };
|
|
37
38
|
export const getCachedSystemEntities = async (client) => {
|
|
38
39
|
const accessToken = useRuntimeConfig()["@laioutr/app-shopware"].accessToken;
|
|
39
40
|
const cache = useUserlandCache(`shopware:${accessToken}:system-entities`);
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
return
|
|
41
|
+
const cached = await cache.readOne("default", entryOptions);
|
|
42
|
+
if (cached && !cached.absent) {
|
|
43
|
+
return cached.value;
|
|
43
44
|
}
|
|
44
45
|
const systemEntities = await getSystemEntities(client);
|
|
45
|
-
|
|
46
|
+
cache.writeOne("default", systemEntities, entryOptions);
|
|
46
47
|
return systemEntities;
|
|
47
48
|
};
|
|
@@ -2,14 +2,15 @@ import { extractShopwareId, isShopwareId } from "./isShopwareId.js";
|
|
|
2
2
|
import { isSlugMatchingSeoPath } from "./mappers/slugMapper.js";
|
|
3
3
|
import { useUserlandCache } from "#imports";
|
|
4
4
|
const SEO_ENTRY_TTL = 60 * 60 * 24;
|
|
5
|
+
const entryOptions = { maxAge: SEO_ENTRY_TTL };
|
|
5
6
|
export const useSeoResolver = (storefrontClient, routeNames) => {
|
|
6
7
|
const cache = useUserlandCache("shopware/seo-urls");
|
|
7
8
|
const resolve = async (type, slug) => {
|
|
8
9
|
const languageId = storefrontClient.defaultHeaders["sw-language-id"] ?? "default";
|
|
9
10
|
const cacheKey = `${languageId}:${type}-${slug}`;
|
|
10
|
-
const
|
|
11
|
-
if (
|
|
12
|
-
return
|
|
11
|
+
const cached = await cache.readOne(cacheKey, entryOptions);
|
|
12
|
+
if (cached && !cached.absent) {
|
|
13
|
+
return cached.value;
|
|
13
14
|
}
|
|
14
15
|
const shopwareId = extractShopwareId(slug);
|
|
15
16
|
if (shopwareId) {
|
|
@@ -57,7 +58,7 @@ export const useSeoResolver = (storefrontClient, routeNames) => {
|
|
|
57
58
|
id: bestMatch.foreignKey,
|
|
58
59
|
matchedPath: bestMatch.seoPathInfo
|
|
59
60
|
};
|
|
60
|
-
|
|
61
|
+
cache.writeOne(cacheKey, entry, entryOptions);
|
|
61
62
|
return entry;
|
|
62
63
|
};
|
|
63
64
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@laioutr/app-shopware",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Laioutr integration with Shopware 6",
|
|
5
5
|
"repository": "github:laioutr/app-shopware",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,10 +22,6 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"dist"
|
|
24
24
|
],
|
|
25
|
-
"publishConfig": {
|
|
26
|
-
"access": "public",
|
|
27
|
-
"provenance": true
|
|
28
|
-
},
|
|
29
25
|
"browserslist": [
|
|
30
26
|
"> 0.5% and not dead"
|
|
31
27
|
],
|
|
@@ -40,12 +36,12 @@
|
|
|
40
36
|
"devDependencies": {
|
|
41
37
|
"@changesets/cli": "^2.31.0",
|
|
42
38
|
"@laioutr-app/ui": "^2.10.6",
|
|
43
|
-
"@laioutr-core/canonical-types": "^0.
|
|
44
|
-
"@laioutr-core/core-types": "^0.
|
|
45
|
-
"@laioutr-core/devtools": "^0.
|
|
46
|
-
"@laioutr-core/frontend-core": "^0.
|
|
47
|
-
"@laioutr-core/kit": "^0.
|
|
48
|
-
"@laioutr-core/orchestr": "^0.
|
|
39
|
+
"@laioutr-core/canonical-types": "^0.33.0",
|
|
40
|
+
"@laioutr-core/core-types": "^0.52.0",
|
|
41
|
+
"@laioutr-core/devtools": "^0.52.0",
|
|
42
|
+
"@laioutr-core/frontend-core": "^0.52.0",
|
|
43
|
+
"@laioutr-core/kit": "^0.52.0",
|
|
44
|
+
"@laioutr-core/orchestr": "^0.52.0",
|
|
49
45
|
"@laioutr/eslint-config": "^1.9.5",
|
|
50
46
|
"@laioutr/prettier-config": "^1.1.0",
|
|
51
47
|
"@nuxt/devtools": "^2.6.3",
|
|
@@ -69,26 +65,30 @@
|
|
|
69
65
|
"vue-tsc": "2.2.10"
|
|
70
66
|
},
|
|
71
67
|
"peerDependencies": {
|
|
72
|
-
"@laioutr-core/canonical-types": ">=0.
|
|
73
|
-
"@laioutr-core/core-types": ">=0.
|
|
74
|
-
"@laioutr-core/frontend-core": ">=0.
|
|
75
|
-
"@laioutr-core/kit": ">=0.
|
|
76
|
-
"@laioutr-core/orchestr": ">=0.
|
|
68
|
+
"@laioutr-core/canonical-types": ">=0.33.0",
|
|
69
|
+
"@laioutr-core/core-types": ">=0.52.0",
|
|
70
|
+
"@laioutr-core/frontend-core": ">=0.52.0",
|
|
71
|
+
"@laioutr-core/kit": ">=0.52.0",
|
|
72
|
+
"@laioutr-core/orchestr": ">=0.52.0"
|
|
77
73
|
},
|
|
78
74
|
"engines": {
|
|
79
75
|
"node": ">=22.12.0",
|
|
80
76
|
"pnpm": ">=10.15.0"
|
|
81
77
|
},
|
|
78
|
+
"publishConfig": {
|
|
79
|
+
"access": "public",
|
|
80
|
+
"provenance": true
|
|
81
|
+
},
|
|
82
82
|
"scripts": {
|
|
83
|
+
"changeset": "changeset",
|
|
83
84
|
"dev": "npm run dev:prepare && nuxi dev playground",
|
|
84
85
|
"dev:build": "nuxi build playground",
|
|
85
86
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
|
|
86
87
|
"lint": "eslint .",
|
|
87
|
-
"changeset": "changeset",
|
|
88
|
-
"version": "changeset version",
|
|
89
88
|
"release": "pnpm run prepack && changeset publish",
|
|
90
89
|
"test": "vitest run",
|
|
91
90
|
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
|
|
92
|
-
"test:watch": "vitest watch"
|
|
91
|
+
"test:watch": "vitest watch",
|
|
92
|
+
"version": "changeset version"
|
|
93
93
|
}
|
|
94
94
|
}
|