@shopbite-de/storefront 1.18.5 → 1.20.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.
@@ -0,0 +1,99 @@
1
+ import type { components } from "~~/api-types/storeApiTypes";
2
+
3
+ type Schemas = components["schemas"];
4
+
5
+ const MAX_OPTION_IDS = 20;
6
+ const MAX_OPTION_ID_LENGTH = 64;
7
+
8
+ function parseOptionIds(raw: unknown): string[] {
9
+ const arr = Array.isArray(raw) ? raw : [raw];
10
+ const seen = new Set<string>();
11
+ for (const v of arr) {
12
+ if (
13
+ typeof v !== "string" ||
14
+ v.trim() === "" ||
15
+ v.length > MAX_OPTION_ID_LENGTH
16
+ )
17
+ continue;
18
+ seen.add(v);
19
+ if (seen.size >= MAX_OPTION_IDS) break;
20
+ }
21
+ return [...seen];
22
+ }
23
+
24
+ export default defineCachedEventHandler(
25
+ async (event): Promise<Schemas["Product"] | null> => {
26
+ const { parentId, optionIds: rawOptionIds } = getQuery(event);
27
+
28
+ if (typeof parentId !== "string" || parentId.trim() === "") {
29
+ throw createError({
30
+ statusCode: 400,
31
+ message: "Missing or invalid parentId",
32
+ });
33
+ }
34
+
35
+ const optionIds = parseOptionIds(rawOptionIds);
36
+ if (optionIds.length === 0) {
37
+ throw createError({
38
+ statusCode: 400,
39
+ message: "At least one optionId is required",
40
+ });
41
+ }
42
+
43
+ const { endpoint, accessToken } = useRuntimeConfig().public.shopware;
44
+
45
+ const filter: Schemas["Filters"] = [
46
+ { type: "equals", field: "parentId", value: parentId },
47
+ ...optionIds.map(
48
+ (id) =>
49
+ ({
50
+ type: "equals",
51
+ field: "optionIds",
52
+ value: id,
53
+ }) as Schemas["EqualsFilter"],
54
+ ),
55
+ ];
56
+
57
+ const response = await $fetch<{ elements?: Schemas["Product"][] }>(
58
+ `${endpoint}/product`,
59
+ {
60
+ method: "POST",
61
+ headers: { "sw-access-key": accessToken },
62
+ body: {
63
+ filter,
64
+ limit: 1,
65
+ includes: {
66
+ product: [
67
+ "id",
68
+ "name",
69
+ "description",
70
+ "translated",
71
+ "productNumber",
72
+ "options",
73
+ "properties",
74
+ "calculatedPrice",
75
+ ],
76
+ product_option: ["id", "groupId", "name", "translated", "group"],
77
+ property: ["id", "name", "translated", "options"],
78
+ property_group_option: ["id", "name", "translated", "group"],
79
+ },
80
+ associations: {
81
+ options: { associations: { group: {} } },
82
+ properties: { associations: { group: {} } },
83
+ },
84
+ },
85
+ },
86
+ );
87
+
88
+ return response.elements?.[0] ?? null;
89
+ },
90
+ {
91
+ maxAge: useRuntimeConfig().public.shopBite.cacheTtl.variant,
92
+ name: "variant",
93
+ getKey: (event) => {
94
+ const { parentId, optionIds } = getQuery(event);
95
+ const ids = parseOptionIds(optionIds).sort();
96
+ return `variant-${parentId}-${ids.join("|")}`;
97
+ },
98
+ },
99
+ );
@@ -1,19 +1,15 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import { useProductConfigurator } from "../../app/composables/useProductConfigurator";
3
3
 
4
- const { mockInvoke, mockConfigurator, mockProduct } = vi.hoisted(() => ({
5
- mockInvoke: vi.fn(),
4
+ const { mockFetch, mockConfigurator, mockProduct } = vi.hoisted(() => ({
5
+ mockFetch: vi.fn(),
6
6
  mockConfigurator: { value: [] },
7
7
  mockProduct: { value: { id: "p1", optionIds: [], options: [] } },
8
8
  }));
9
9
 
10
- // Use vi.mock for explicit imports
10
+ vi.stubGlobal("$fetch", mockFetch);
11
+
11
12
  vi.mock("@shopware/composables", () => ({
12
- useShopwareContext: () => ({
13
- apiClient: {
14
- invoke: mockInvoke,
15
- },
16
- }),
17
13
  useProductConfigurator: () => ({
18
14
  handleChange: vi.fn(),
19
15
  }),
@@ -57,24 +53,25 @@ describe("useProductConfigurator", () => {
57
53
  mockProduct.value = {
58
54
  parentId: "parent-1",
59
55
  } as unknown as typeof mockProduct.value;
60
- mockInvoke.mockResolvedValue({
61
- data: {
62
- elements: [{ id: "variant-1" }],
63
- },
64
- });
56
+ mockFetch.mockResolvedValue({ id: "variant-1" });
65
57
 
66
58
  const { findVariantForSelectedOptions } = useProductConfigurator();
67
59
  const result = await findVariantForSelectedOptions({ Size: "o1" });
68
60
 
69
- expect(mockInvoke).toHaveBeenCalledWith(
70
- "readProduct post /product",
71
- expect.any(Object),
61
+ expect(mockFetch).toHaveBeenCalledWith(
62
+ "/api/product/variant",
63
+ expect.objectContaining({
64
+ query: {
65
+ parentId: "parent-1",
66
+ optionIds: ["o1"],
67
+ },
68
+ }),
72
69
  );
73
70
  expect(result).toEqual({ id: "variant-1" });
74
71
  });
75
72
 
76
73
  it("should return undefined on error in findVariantForSelectedOptions", async () => {
77
- mockInvoke.mockRejectedValue(new Error("API Error"));
74
+ mockFetch.mockRejectedValue(new Error("API Error"));
78
75
  const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
79
76
 
80
77
  const { findVariantForSelectedOptions } = useProductConfigurator();