@meeovi/commerce 1.0.0 → 1.0.2

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 ADDED
@@ -0,0 +1,66 @@
1
+ # @meeovi/commerce
2
+
3
+ A backend‑agnostic commerce domain module for the Meeovi ecosystem.
4
+ This package provides a unified interface for products, categories, carts, and other commerce operations — regardless of the underlying backend (Directus, Medusa, Shopify, custom APIs, etc.).
5
+
6
+ ## ✨ Features
7
+
8
+ - Pluggable provider architecture
9
+ - Unified composables (`useProducts`, `useCart`, `useCategories`)
10
+ - Backend‑agnostic types
11
+ - Runtime configuration
12
+ - Zero Nuxt dependency
13
+ - Works in any JS/TS environment
14
+
15
+ ## 📦 Installation
16
+
17
+ ```sh
18
+ npm install @meeovi/commerce
19
+
20
+ ⚙️ Configuration
21
+ Configure the active providers at runtime:
22
+
23
+
24
+ import { setCommerceConfig } from '@meeovi/commerce'
25
+
26
+ setCommerceConfig({
27
+ productProvider: 'directus',
28
+ cartProvider: 'directus',
29
+ categoryProvider: 'directus'
30
+ })
31
+
32
+ 🧩 Usage
33
+
34
+ import { useProducts } from '@meeovi/commerce'
35
+
36
+ const { listProducts, getProduct } = useProducts()
37
+
38
+ const products = await listProducts()
39
+ const product = await getProduct('123')
40
+ 🔌 Providers
41
+ A provider implements one or more domain interfaces:
42
+
43
+
44
+ export interface ProductProvider {
45
+ getProduct(id: string): Promise<Product>
46
+ listProducts(params?: any): Promise<Product[]>
47
+ }
48
+ Register a provider:
49
+
50
+
51
+ import { registerProductProvider } from '@meeovi/commerce'
52
+
53
+ registerProductProvider('directus', {
54
+ getProduct: async (id) => { ... },
55
+ listProducts: async () => { ... }
56
+ })
57
+
58
+ 🧱 Folder Structure
59
+ Code
60
+ src/
61
+ products/
62
+ cart/
63
+ categories/
64
+ config.
65
+ registry.
66
+ useCommerce.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meeovi/commerce",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Commerce module for the M Framework.",
5
5
  "license": "ISC",
6
6
  "author": "Meeovi",
@@ -12,6 +12,8 @@
12
12
  "dependencies": {
13
13
  "@better-auth/stripe": "^1.4.15",
14
14
  "@polar-sh/better-auth": "^1.6.4",
15
- "@polar-sh/sdk": "^0.42.2"
15
+ "@polar-sh/sdk": "^0.42.2",
16
+ "@storefront-ui/nuxt": "^3.1.1",
17
+ "@vue-storefront/unified-data-model": "^3.0.0"
16
18
  }
17
19
  }
@@ -0,0 +1,218 @@
1
+ import { ref, computed, readonly } from 'vue'
2
+ import type { Cart, CartItem, Product } from '../types'
3
+ import { useCart } from '../composables/cart/useCart'
4
+ import { defineStore } from 'pinia'
5
+
6
+ export const useCartStore = defineStore('cart', () => {
7
+ const cart = ref<Cart | null>(null)
8
+ const loading = ref(false)
9
+ const error = ref<string | null>(null)
10
+
11
+ // Import cart composable
12
+ const {
13
+ fetchCart,
14
+ addToCart,
15
+ removeFromCart,
16
+ updateCartItem,
17
+ applyCoupon,
18
+ removeCoupon,
19
+ clearCart,
20
+ setShippingOption: setShippingOptionFn
21
+ ,createCheckoutSession: createCheckoutSessionFn
22
+ } = useCart()
23
+
24
+ const initializeCart = async () => {
25
+ loading.value = true
26
+ error.value = null
27
+ try {
28
+ await fetchCart()
29
+ } catch (err: any) {
30
+ error.value = err.message
31
+ console.error('Error initializing cart:', err)
32
+ } finally {
33
+ loading.value = false
34
+ }
35
+ }
36
+
37
+ const addProductToCart = async (productId: string, quantity: number = 1, variantId?: string) => {
38
+ loading.value = true
39
+ error.value = null
40
+ try {
41
+ await addToCart(productId, quantity, variantId)
42
+ } catch (err: any) {
43
+ error.value = err.message
44
+ console.error('Error adding product to cart:', err)
45
+ throw err
46
+ } finally {
47
+ loading.value = false
48
+ }
49
+ }
50
+
51
+ const removeProductFromCart = async (itemId: string) => {
52
+ loading.value = true
53
+ error.value = null
54
+ try {
55
+ await removeFromCart(itemId)
56
+ } catch (err: any) {
57
+ error.value = err.message
58
+ console.error('Error removing product from cart:', err)
59
+ throw err
60
+ } finally {
61
+ loading.value = false
62
+ }
63
+ }
64
+
65
+ const updateProductQuantity = async (itemId: string, quantity: number) => {
66
+ loading.value = true
67
+ error.value = null
68
+ try {
69
+ await updateCartItem(itemId, quantity)
70
+ } catch (err: any) {
71
+ error.value = err.message
72
+ console.error('Error updating cart item:', err)
73
+ throw err
74
+ } finally {
75
+ loading.value = false
76
+ }
77
+ }
78
+
79
+ const applyCartCoupon = async (couponCode: string) => {
80
+ loading.value = true
81
+ error.value = null
82
+ try {
83
+ const success = await applyCoupon(couponCode)
84
+ if (!success) {
85
+ throw new Error('Failed to apply coupon')
86
+ }
87
+ } catch (err: any) {
88
+ error.value = err.message
89
+ console.error('Error applying coupon:', err)
90
+ throw err
91
+ } finally {
92
+ loading.value = false
93
+ }
94
+ }
95
+
96
+ const removeCartCoupon = async () => {
97
+ loading.value = true
98
+ error.value = null
99
+ try {
100
+ await removeCoupon()
101
+ } catch (err: any) {
102
+ error.value = err.message
103
+ console.error('Error removing coupon:', err)
104
+ throw err
105
+ } finally {
106
+ loading.value = false
107
+ }
108
+ }
109
+
110
+ const emptyCart = async () => {
111
+ loading.value = true
112
+ error.value = null
113
+ try {
114
+ await clearCart()
115
+ } catch (err: any) {
116
+ error.value = err.message
117
+ console.error('Error clearing cart:', err)
118
+ throw err
119
+ } finally {
120
+ loading.value = false
121
+ }
122
+ }
123
+
124
+ const setShippingOption = async (option: any) => {
125
+ loading.value = true
126
+ error.value = null
127
+ try {
128
+ if (!setShippingOptionFn) throw new Error('Shipping setter unavailable')
129
+ await setShippingOptionFn(option)
130
+ } catch (err: any) {
131
+ error.value = err.message
132
+ console.error('Error setting shipping option:', err)
133
+ throw err
134
+ } finally {
135
+ loading.value = false
136
+ }
137
+ }
138
+
139
+ const createCheckoutSession = async (cartId?: string) => {
140
+ loading.value = true
141
+ error.value = null
142
+ try {
143
+ const data = await createCheckoutSessionFn(cartId)
144
+ return data
145
+ } catch (err: any) {
146
+ error.value = err.message
147
+ throw err
148
+ } finally {
149
+ loading.value = false
150
+ }
151
+ }
152
+
153
+ // Computed properties
154
+ const itemCount = computed(() => {
155
+ return cart.value?.items?.reduce((count: any, item: { quantity: any }) => count + item.quantity, 0) || 0
156
+ })
157
+
158
+ const isEmpty = computed(() => {
159
+ return !cart.value?.items?.length
160
+ })
161
+
162
+ const hasItems = computed(() => {
163
+ return !isEmpty.value
164
+ })
165
+
166
+ const subtotal = computed(() => {
167
+ return cart.value?.subtotal || 0
168
+ })
169
+
170
+ const total = computed(() => {
171
+ return cart.value?.total || 0
172
+ })
173
+
174
+ const taxAmount = computed(() => {
175
+ return cart.value?.tax_amount || 0
176
+ })
177
+
178
+ const shippingAmount = computed(() => {
179
+ return cart.value?.shipping_amount || 0
180
+ })
181
+
182
+ const discountAmount = computed(() => {
183
+ return cart.value?.discount_amount || 0
184
+ })
185
+
186
+ const hasCoupon = computed(() => {
187
+ return !!cart.value?.coupon_code
188
+ })
189
+
190
+ return {
191
+ // State
192
+ cart: readonly(cart),
193
+ loading: readonly(loading),
194
+ error: readonly(error),
195
+
196
+ // Computed
197
+ itemCount,
198
+ isEmpty,
199
+ hasItems,
200
+ subtotal,
201
+ total,
202
+ taxAmount,
203
+ shippingAmount,
204
+ discountAmount,
205
+ hasCoupon,
206
+
207
+ // Actions
208
+ initializeCart,
209
+ addProductToCart,
210
+ removeProductFromCart,
211
+ updateProductQuantity,
212
+ applyCartCoupon,
213
+ removeCartCoupon,
214
+ emptyCart
215
+ ,setShippingOption
216
+ ,createCheckoutSession
217
+ }
218
+ })
@@ -0,0 +1,300 @@
1
+ // stores/cartStore.ts
2
+ import { defineStore } from 'pinia'
3
+ import { MagentoService } from '~/services/magento'
4
+
5
+ interface MagentoProduct {
6
+ sku: string
7
+ name: string
8
+ price: number
9
+ qty: number
10
+ quote_id?: string
11
+ item_id?: string
12
+ }
13
+
14
+ interface CartState {
15
+ isGuest: boolean
16
+ items: MagentoProduct[]
17
+ total: number
18
+ quoteId: string | null
19
+ magentoService: MagentoService
20
+ }
21
+
22
+
23
+ export const useCartStore = defineStore('cart', {
24
+ state: () => ({
25
+ isGuest: true,
26
+ items: [] as MagentoProduct[],
27
+ total: 0,
28
+ quoteId: null as string | null,
29
+ magentoService: new MagentoService()
30
+ }),
31
+
32
+ actions: {
33
+ async initializeCart() {
34
+ const auth = useAuth()
35
+ this.isGuest = !auth.token.value
36
+
37
+ try {
38
+ if (this.isGuest) {
39
+ await this.createGuestCart()
40
+ } else {
41
+ await this.createCustomerCart()
42
+ }
43
+ } catch (error) {
44
+ errorHandler.handle(error)
45
+ }
46
+ },
47
+
48
+ async createGuestCart() {
49
+ const response = await fetch(`${process.env.MAGENTO_API_URL}/guest-carts`, {
50
+ method: 'POST'
51
+ })
52
+ const quoteId = await response.json()
53
+ this.quoteId = quoteId as string
54
+ },
55
+
56
+ async createCustomerCart() {
57
+ const auth = useAuth()
58
+ const response = await fetch(`${process.env.MAGENTO_API_URL}/carts/mine`, {
59
+ method: 'POST',
60
+ headers: {
61
+ 'Authorization': `Bearer ${auth.token.value}`
62
+ }
63
+ })
64
+ this.quoteId = await response.json()
65
+ },
66
+
67
+ async addItem(product: MagentoProduct) {
68
+ const loading = useLoading()
69
+ loading.startLoading('Adding to cart...')
70
+
71
+ try {
72
+ // Check authentication
73
+ const auth = useAuth()
74
+ if (auth.isTokenExpired()) {
75
+ await auth.refreshAccessToken()
76
+ }
77
+
78
+ // Check inventory
79
+ const inventory = useInventory()
80
+ await inventory.checkInventory(product.sku, product.qty)
81
+
82
+ // If no quote ID, create cart
83
+ if (!this.quoteId) {
84
+ await this.initializeCart()
85
+ }
86
+
87
+ // Ensure quoteId exists before proceeding
88
+ if (!this.quoteId) {
89
+ throw new CartError('Failed to create cart', 'CART_ERROR')
90
+ }
91
+
92
+ // Add to Magento cart
93
+ const response = await fetch(`${process.env.MAGENTO_API_URL}/carts/mine/items`, {
94
+ method: 'POST',
95
+ headers: {
96
+ 'Authorization': `Bearer ${auth.token.value}`,
97
+ 'Content-Type': 'application/json'
98
+ },
99
+ body: JSON.stringify({
100
+ cartItem: {
101
+ sku: product.sku,
102
+ qty: product.qty,
103
+ quote_id: this.quoteId
104
+ }
105
+ })
106
+ })
107
+
108
+ if (!response.ok) {
109
+ throw new CartError('Failed to add item to cart', 'CART_ERROR')
110
+ }
111
+
112
+ const cartItem = await response.json()
113
+
114
+ // Update local cart state
115
+ const existingItem = this.items.find(item => item.sku === product.sku)
116
+ if (existingItem) {
117
+ existingItem.qty += product.qty
118
+ } else {
119
+ this.items.push({
120
+ ...product,
121
+ item_id: cartItem.item_id
122
+ })
123
+ }
124
+
125
+ // Calculate taxes
126
+ const tax = useTax()
127
+ if (this.quoteId) {
128
+ await tax.calculateTax(this.quoteId)
129
+ }
130
+
131
+ // Cache product data
132
+ const cache = useCache()
133
+ cache.setCacheItem(`cart_product_${product.sku}`, product)
134
+
135
+ await this.calculateTotal()
136
+
137
+ loading.stopLoading()
138
+ const { show } = useNotification()
139
+ show({
140
+ type: 'success',
141
+ message: 'Product added to cart'
142
+ })
143
+ } catch (error) {
144
+ loading.stopLoading()
145
+ errorHandler.handle(error)
146
+ }
147
+ },
148
+
149
+ async removeItem(itemId: string) {
150
+ const loading = useLoading()
151
+ loading.startLoading('Removing item...')
152
+
153
+ try {
154
+ const auth = useAuth()
155
+ if (auth.isTokenExpired()) {
156
+ await auth.refreshAccessToken()
157
+ }
158
+
159
+ // Remove from Magento cart
160
+ const response = await fetch(
161
+ `${process.env.MAGENTO_API_URL}/carts/mine/items/${itemId}`,
162
+ {
163
+ method: 'DELETE',
164
+ headers: {
165
+ 'Authorization': `Bearer ${auth.token.value}`
166
+ }
167
+ }
168
+ )
169
+
170
+ if (!response.ok) {
171
+ throw new CartError('Failed to remove item from cart', 'CART_ERROR')
172
+ }
173
+
174
+ // Update local cart state
175
+ this.items = this.items.filter(item => item.item_id !== itemId)
176
+ await this.calculateTotal()
177
+
178
+ loading.stopLoading()
179
+ useNotification().show({
180
+ type: 'success',
181
+ message: 'Item removed from cart'
182
+ })
183
+ } catch (error) {
184
+ loading.stopLoading()
185
+ errorHandler.handle(error)
186
+ }
187
+ },
188
+
189
+ async calculateTotal() {
190
+ try {
191
+ const auth = useAuth()
192
+ if (!this.quoteId) return
193
+
194
+ // Get cart totals from Magento
195
+ const response = await fetch(
196
+ `${process.env.MAGENTO_API_URL}/carts/mine/totals`,
197
+ {
198
+ headers: {
199
+ 'Authorization': `Bearer ${auth.token.value}`
200
+ }
201
+ }
202
+ )
203
+
204
+ if (!response.ok) {
205
+ throw new CartError('Failed to get cart totals', 'CART_ERROR')
206
+ }
207
+
208
+ const totals = await response.json()
209
+ this.total = totals.grand_total
210
+ } catch (error) {
211
+ console.error('Error calculating totals:', error)
212
+ errorHandler.handle(error)
213
+ }
214
+ },
215
+
216
+ // Update other methods to use the notification composable
217
+ async clearCart() {
218
+ const loading = useLoading()
219
+ loading.startLoading('Clearing cart...')
220
+
221
+ try {
222
+ const auth = useAuth()
223
+ if (this.quoteId) {
224
+ const response = await fetch(`${process.env.MAGENTO_API_URL}/carts/mine/clear`, {
225
+ method: 'POST',
226
+ headers: {
227
+ 'Authorization': `Bearer ${auth.token.value}`
228
+ }
229
+ })
230
+
231
+ if (!response.ok) {
232
+ throw new CartError('Failed to clear cart', 'CART_ERROR')
233
+ }
234
+ }
235
+
236
+ this.items = []
237
+ this.total = 0
238
+ this.quoteId = null
239
+
240
+ // Clear cart-related cache
241
+ const cache = useCache()
242
+ this.items.forEach(item => {
243
+ cache.setCacheItem(`cart_product_${item.sku}`, null)
244
+ })
245
+
246
+ loading.stopLoading()
247
+ const { show } = useNotification()
248
+ show({
249
+ type: 'success',
250
+ message: 'Cart cleared successfully'
251
+ })
252
+ } catch (error) {
253
+ loading.stopLoading()
254
+ errorHandler.handle(error)
255
+ }
256
+ },
257
+
258
+ async syncCartWithMagento() {
259
+ try {
260
+ const auth = useAuth()
261
+ const response = await fetch(`${process.env.MAGENTO_API_URL}/carts/mine/items`, {
262
+ headers: {
263
+ 'Authorization': `Bearer ${auth.token.value}`
264
+ }
265
+ })
266
+
267
+ if (!response.ok) {
268
+ throw new CartError('Failed to sync cart', 'CART_ERROR')
269
+ }
270
+
271
+ const magentoItems = await response.json()
272
+ this.items = magentoItems
273
+ await this.calculateTotal()
274
+ } catch (error) {
275
+ errorHandler.handle(error)
276
+ }
277
+ },
278
+
279
+ async validateCart() {
280
+ try {
281
+ // Check all items inventory
282
+ for (const item of this.items) {
283
+ const inventory = useInventory()
284
+ const isAvailable = await inventory.checkInventory(item.sku, item.qty)
285
+ if (!isAvailable) {
286
+ throw new CartError(`${item.name} is out of stock`, 'INVENTORY_ERROR')
287
+ }
288
+ }
289
+
290
+ // Validate prices
291
+ await this.syncCartWithMagento()
292
+
293
+ return true
294
+ } catch (error) {
295
+ errorHandler.handle(error)
296
+ return false
297
+ }
298
+ }
299
+ }
300
+ })
@@ -0,0 +1,19 @@
1
+ import { defineStore } from 'pinia';
2
+ import type { CheckoutState } from '@/types/checkout';
3
+
4
+ export const useCheckoutStore = defineStore('checkout', {
5
+ state: (): CheckoutState => ({
6
+ shippingAddress: null,
7
+ paymentMethod: null,
8
+ orderId: '',
9
+ isLoading: false
10
+ }),
11
+ actions: {
12
+ setShippingAddress(address: string) {
13
+ this.shippingAddress = address;
14
+ },
15
+ setPaymentMethod(method: string) {
16
+ this.paymentMethod = method;
17
+ }
18
+ }
19
+ });
@@ -0,0 +1,65 @@
1
+ // stores/compare.ts
2
+ import { defineStore } from 'pinia';
3
+ import type {
4
+ ComparableAttribute,
5
+ ComparableItem,
6
+ ComparableProduct,
7
+ CompareList,
8
+ } from '~/graphql/queries-mutations_subscriptions/types/ProductCompare.type';
9
+
10
+ export const useCompareStore = defineStore('compare', {
11
+ state: () => ({
12
+ isLoading: false,
13
+ count: 0,
14
+ attributes: [] as ComparableAttribute[],
15
+ products: [] as ComparableProduct[],
16
+ productSkus: [] as string[],
17
+ items: [] as ComparableItem[]
18
+ }),
19
+
20
+ actions: {
21
+ toggleLoader(isLoading: boolean) {
22
+ this.isLoading = isLoading;
23
+ },
24
+
25
+ setCompareList(payload: CompareList) {
26
+ this.attributes = payload.attributes || [];
27
+ this.products = payload.products || [];
28
+ this.items = payload.items || [];
29
+ },
30
+
31
+ removeComparedProduct(productSku: string) {
32
+ this.products = this.products.filter(product => product.sku !== productSku);
33
+ this.productSkus = this.productSkus.filter(sku => sku !== productSku);
34
+ this.items = this.items.filter(item => item.product.sku !== productSku);
35
+ },
36
+
37
+ clearComparedProducts() {
38
+ this.products = [];
39
+ this.productSkus = [];
40
+ this.items = [];
41
+ this.count = 0;
42
+ },
43
+
44
+ setCompareListSkus(productSkus: string[]) {
45
+ this.productSkus = productSkus;
46
+ },
47
+
48
+ addComparedProductSku(productSku: string) {
49
+ if (!this.productSkus.includes(productSku)) {
50
+ this.productSkus.push(productSku);
51
+ }
52
+ },
53
+
54
+ updateCompareTotals(compareTotals: string) {
55
+ this.count = parseInt(compareTotals, 10); // Convert string to number
56
+ }
57
+ },
58
+
59
+ getters: {
60
+ getCompareCount: (state) => state.count,
61
+ getComparedProducts: (state) => state.products,
62
+ getComparedProductSkus: (state) => state.productSkus,
63
+ getIsLoading: (state) => state.isLoading
64
+ }
65
+ });
@@ -0,0 +1,29 @@
1
+ import { defineStore } from 'pinia'
2
+
3
+ export const useCurrencyStore = defineStore('currency', {
4
+ state: () => ({
5
+ currentCurrency: null,
6
+ exchangeRates: {}
7
+ }),
8
+
9
+ actions: {
10
+ setCurrency(currency) {
11
+ this.currentCurrency = currency
12
+ },
13
+
14
+ setExchangeRates(rates) {
15
+ this.exchangeRates = rates
16
+ },
17
+
18
+ async updateCurrency(currency) {
19
+ this.setCurrency(currency)
20
+ // Here you would typically make an API call to update the user's preference
21
+ // if they are logged in
22
+ }
23
+ },
24
+
25
+ getters: {
26
+ getCurrentCurrency: (state) => state.currentCurrency,
27
+ getExchangeRates: (state) => state.exchangeRates
28
+ }
29
+ })