diff --git a/README.md b/README.md
index c9b88d7..9815e5e 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,93 @@
-# @laioutr-app/shopware
+# 🛍️ Laioutr Shopware Integration
-> Shopware integration for laioutr
+This repository contains the official Shopware 6 integration for the Laioutr frontend framework. This connector provides a comprehensive set of features to connect your Laioutr application with a Shopware backend, enabling full e-commerce functionality.
-## Usage
+This connector provides a robust bridge to Shopware as it handles sessions, customer data, cart management, product retrieval, reviews, and more.
-Example package usage
+
-In your nuxt.config.ts:
+### ✨ Features:
-```ts
+- 👤 Customer & Session Management
+
+ - Anonymous Sessions: Track users and persist carts before they log in.
+ - Login & Customer Sessions: Full support for registered customer authentication and session handling.
+
+- 🛒 Cart Management
+
+ - Get Current Cart: Retrieve the active shopping cart for the current session.
+ - Add Item to Cart: Seamlessly add products and variants to the user's cart.
+
+- 🗂️ Category & Navigation
+
+ - Category List: Fetch a flat list of all available categories.
+ - Category Tree: Retrieve a nested category structure by its alias (e.g., for building navigation menus).
+
+- 📦 Products & Variants
+
+ - Products by Category ID: Get a list of all products within a specific category using its ID.
+ - Products by Category Slug: Get a list of all products within a specific category using its URL slug.
+ - Get Product by Slug: Retrieve detailed information for a single product.
+ - Product Variants: Fetch all available variants (e.g., size, color) for a product.
+ - Canonical Categories: Identify the primary (canonical) category for a product, essential for SEO.
+ - Product Search: Implement powerful, native Shopware search functionality across your product catalog.
+
+- 🔍 Search & Reviews
+
+ - Get Reviews: Retrieve all customer reviews for a specific product.
+ - Create Reviews: Allow logged-in customers to submit new product reviews.
+
+- ✉️ Miscellaneous
+ - Newsletter Subscription: Enable users to subscribe to marketing newsletters.
+
+
+
+### 🚀 Installation
+
+```bash
+# Using npm
+npm install @laioutr/app-shopware
+
+# Using yarn
+yarn add @laioutr/app-shopware
+```
+
+### ⚙️ Configuration & Usage
+
+To get started, you need to configure the connector with your Shopware API credentials inside `nuxt.config.ts`. We recommend using environment variables:
+
+```typescript
defineNuxtConfig({
/* [...] */
- modules: ['@laioutr-app/shopware'],
+ modules: ['@laioutr/app-shopware'],
+ /* [...] */
+ '@laioutr/app-shopware': {
+ endpoint: import.meta.env.SHOPWARE_DEMO_ENDPOINT,
+ accessToken: import.meta.env.SHOPWARE_DEMO_ACCESS_TOKEN,
+ adminEndpoint: import.meta.env.SHOPWARE_DEMO_ADMIN_ENDPOINT,
+ adminClientId: import.meta.env.SHOPWARE_DEMO_ADMIN_CLIENT_ID,
+ adminClientSecret: import.meta.env.SHOPWARE_DEMO_ADMIN_CLIENT_SECRET,
+ },
/* [...] */
});
```
+
+
+
+### 🤝 Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request or open an issue for bugs, feature requests, or improvements.
+
+Fork the repository.
+
+Create your feature branch (git checkout -b feature/AmazingFeature).
+
+Commit your changes (git commit -m 'feat: Add some AmazingFeature').
+
+Push to the branch (git push origin feature/AmazingFeature).
+
+Open a Pull Request.
+
+### 📄 License
+
+This project is licensed under the MIT License. See the LICENSE file for details.
diff --git a/src/runtime/server/const/passthroughTokens.ts b/src/runtime/server/const/passthroughTokens.ts
index be61cb2..99f521d 100644
--- a/src/runtime/server/const/passthroughTokens.ts
+++ b/src/runtime/server/const/passthroughTokens.ts
@@ -1,5 +1,5 @@
import { createPassthroughToken } from '#imports';
-import { ShopwareCategory, ShopwareProduct } from '../types/shopware';
+import { ShopwareCart, ShopwareCategory, ShopwareProduct } from '../types/shopware';
export const currentProductIdsToken = createPassthroughToken('@laioutr/app-shopware/currentProductIdsFragment');
@@ -19,3 +19,5 @@ export const parentIdToDefaultVariantIdToken = createPassthroughToken('@laioutr/app-shopware/productsFragment');
export const productVariantsToken = createPassthroughToken('@laioutr/app-shopware/productVariants');
+
+export const cartFragmentToken = createPassthroughToken('@laioutr-app/shopify/cartFragment');
diff --git a/src/runtime/server/orchestr/cart-item/base.resolver.ts b/src/runtime/server/orchestr/cart-item/base.resolver.ts
new file mode 100644
index 0000000..cd01f25
--- /dev/null
+++ b/src/runtime/server/orchestr/cart-item/base.resolver.ts
@@ -0,0 +1,117 @@
+import { Money } from '@screeny05/ts-money';
+import { MeasurementUnit } from '@laioutr-core/canonical-types';
+import {
+ CartItemAvailability,
+ CartItemBase,
+ CartItemCost,
+ CartItemProductData,
+ CartItemQuantityRule,
+} from '@laioutr-core/canonical-types/entity/cart-item';
+import { cartFragmentToken } from '../../const/passthroughTokens';
+import { defineShopwareComponentResolver } from '../../middleware/defineShopware';
+import { mapMedia } from '../../shopware-helper/mediaMapper';
+
+export default defineShopwareComponentResolver({
+ entityType: 'CartItem',
+ label: 'Shopify Cart Item Component Resolver',
+ provides: [CartItemBase, CartItemCost, CartItemProductData, CartItemAvailability, CartItemQuantityRule],
+ resolve: async ({ $entity, passthrough, clientEnv }) => {
+ const { currency } = clientEnv;
+
+ const cart = passthrough.require(cartFragmentToken);
+
+ const lineItems = (cart.lineItems ?? []).filter((item) => item.type === 'product');
+ const discounts = (cart.lineItems ?? []).filter((item) => item.type === 'discount');
+
+ const mappedDiscounts = discounts.map((item) =>
+ $entity({
+ id: item.id,
+ base: () => ({
+ type: 'discount-code' as const,
+ quantity: item.quantity,
+ title: item.label as string,
+ }),
+ cost: () => ({
+ single: { amount: 0, currency: clientEnv.currency },
+ subtotal: { amount: 0, currency: clientEnv.currency },
+ total: { amount: 0, currency: clientEnv.currency },
+ }),
+ productData: () => undefined,
+ availability: () => ({
+ quantity: 1,
+ status: item.payload.available && item.payload.active ? 'inStock' : 'outOfStock',
+ }),
+ quantityRule: () => ({
+ canChange: false,
+ increment: item.quantityInformation?.purchaseSteps ?? 1,
+ min: item.quantityInformation?.minPurchase ?? 1,
+ }),
+ })
+ );
+
+ const mappedProducts = lineItems.map((item) =>
+ $entity({
+ id: item.id,
+ base: () => ({
+ type: 'product' as const,
+ quantity: item.quantity,
+ title: item.label as string,
+ subtitle: item.payload.name,
+ brand: item.payload.manufacturerNumber,
+ code: item.uniqueIdentifier,
+ cover: item.cover?.media ? { ...mapMedia(item.cover.media), type: 'image' } : undefined,
+ link: {
+ type: 'reference',
+ reference: {
+ type: 'product',
+ slug: item.payload.productNumber,
+ id: item.id,
+ },
+ },
+ }),
+ cost: () => {
+ const listPrice = item.price?.listPrice?.price ?? 0;
+ const unitPrice = item.price?.unitPrice ?? 0;
+ const totalPrice = item.price?.totalPrice ?? 0;
+ const subtotal = Money.fromDecimal({ amount: listPrice * (item.quantity || 1), currency });
+ const total = Money.fromDecimal({ amount: totalPrice, currency });
+ const singleTotal = total.divide(item.quantity || 1);
+ const singleStrikethrough = Money.fromDecimal({ amount: listPrice, currency });
+ const hasStrikethrogh = item.price?.listPrice?.discount && item.price.listPrice.discount < 0;
+
+ return {
+ single: singleTotal,
+ singleStrikethrough: hasStrikethrogh && listPrice > unitPrice ? singleStrikethrough : undefined,
+ subtotal,
+ total,
+ };
+ },
+ productData: () => ({
+ ...(item.payload.packUnit && item.payload.purchaseUnit && item.payload.referenceUnit ?
+ {
+ unitPrice: {
+ price: Money.fromDecimal({ amount: item.price?.unitPrice ?? 0, currency }),
+ quantity: { unit: item.payload.packUnit as MeasurementUnit, value: item.payload.purchaseUnit ?? 0 },
+ reference: { unit: item.payload.packUnit as MeasurementUnit, value: item.payload.referenceUnit ?? 0 },
+ },
+ }
+ : {}),
+ }),
+ availability: () => ({
+ quantity: item.payload.stock ?? 0, // Keep stock level
+ status: item.payload.available ? 'inStock' : 'outOfStock', // Use 'available' flag
+ }),
+ quantityRule: () => ({
+ increment: item.quantityInformation?.purchaseSteps ?? 1,
+ min: item.quantityInformation?.minPurchase ?? 1, // Default to 1
+ max: item.quantityInformation?.maxPurchase ?? Number.MAX_SAFE_INTEGER,
+ canChange: true, // Products should be changeable
+ }),
+ })
+ );
+
+ return {
+ entities: [...mappedDiscounts, ...mappedProducts],
+ };
+ },
+});
diff --git a/src/runtime/server/orchestr/cart/base.resolver.ts b/src/runtime/server/orchestr/cart/base.resolver.ts
index dd53681..2edd240 100644
--- a/src/runtime/server/orchestr/cart/base.resolver.ts
+++ b/src/runtime/server/orchestr/cart/base.resolver.ts
@@ -1,3 +1,4 @@
+import { Money } from '@screeny05/ts-money';
import { CartBase, CartCost } from '@laioutr-core/canonical-types/entity/cart';
import { defineShopwareComponentResolver } from '../../middleware/defineShopware';
@@ -12,7 +13,7 @@ export default defineShopwareComponentResolver({
const cart = await storefrontClient.invoke('readCart get /checkout/cart');
// helper to build Money objects
- const money = (amount: number, currency: string) => ({ amount, currency });
+ const money = (amount: number, currency: string) => Money.fromDecimal({ amount, currency });
// safe defaults
const lineItems = cart.data?.lineItems ?? [];
diff --git a/src/runtime/server/orchestr/cart/cart-item.link.ts b/src/runtime/server/orchestr/cart/cart-item.link.ts
new file mode 100644
index 0000000..ca2d565
--- /dev/null
+++ b/src/runtime/server/orchestr/cart/cart-item.link.ts
@@ -0,0 +1,19 @@
+import { CartItemsLink } from '@laioutr-core/canonical-types/ecommerce';
+import { cartFragmentToken } from '../../const/passthroughTokens';
+import { defineShopwareLink } from '../../middleware/defineShopware';
+
+export default defineShopwareLink(CartItemsLink, async ({ passthrough }) => {
+ const cart = passthrough.require(cartFragmentToken);
+
+ const lienItems = (cart.lineItems ?? []).filter((item) => item.type === 'product').map((item) => item.id);
+ const discounts = (cart.lineItems ?? []).filter((item) => item.type === 'discount').map((item) => item.id);
+
+ return {
+ links: [
+ {
+ sourceId: cart.token as string,
+ targetIds: [...lienItems, ...discounts],
+ },
+ ],
+ };
+});
diff --git a/src/runtime/server/orchestr/cart/get-current.query.ts b/src/runtime/server/orchestr/cart/get-current.query.ts
index a88a12e..edbe82f 100644
--- a/src/runtime/server/orchestr/cart/get-current.query.ts
+++ b/src/runtime/server/orchestr/cart/get-current.query.ts
@@ -1,11 +1,14 @@
import { GetCurrentCartQuery } from '@laioutr-core/canonical-types/ecommerce';
+import { cartFragmentToken } from '../../const/passthroughTokens';
import { defineShopwareQuery } from '../../middleware/defineShopware';
-export default defineShopwareQuery(GetCurrentCartQuery, async ({ context }) => {
+export default defineShopwareQuery(GetCurrentCartQuery, async ({ context, passthrough }) => {
const { storefrontClient } = context;
const cart = await storefrontClient.invoke('readCart get /checkout/cart');
+ passthrough.set(cartFragmentToken, cart.data);
+
/* Cart is identified per unique context session */
return { id: cart.data.token ?? '' };
});
diff --git a/src/runtime/server/types/shopware.ts b/src/runtime/server/types/shopware.ts
index ab62057..3404ce4 100644
--- a/src/runtime/server/types/shopware.ts
+++ b/src/runtime/server/types/shopware.ts
@@ -8,6 +8,8 @@ export type ShopwareFilters = components['schemas']['Filters'];
export type ShopwareManufacturer = components['schemas']['ProductManufacturer'];
export type ShopwareSeoUrl = components['schemas']['SeoUrl'];
export type StorefrontClient = ReturnType>;
+export type ShopwareCart = components['schemas']['Cart'];
+export type ShopwareCartLineItem = Required['items'][number];
export type ShopwareIncludesQuery = components['schemas']['Include'];
export type ShopwareAssociationsQuery = components['schemas']['Association'];