@thorprovider/adapters 3.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/.turbo/turbo-build.log +18 -0
- package/.turbo/turbo-test.log +1 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +271 -0
- package/README.md +634 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.mts +2524 -0
- package/dist/index.d.ts +2524 -0
- package/dist/index.js +8136 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +8045 -0
- package/dist/index.mjs.map +1 -0
- package/docs/RETRY_SOLUTION_SUMMARY.md +55 -0
- package/docs/authentication.md +165 -0
- package/docs/capabilities-system.md +143 -0
- package/docs/category-customization.md +309 -0
- package/docs/fetch-reliability-solution.md +399 -0
- package/docs/getting-started.md +154 -0
- package/docs/multi-tenant-channel-scoping.md +225 -0
- package/docs/profile-management.md +147 -0
- package/docs/providers-overview.md +85 -0
- package/docs/services-overview.md +135 -0
- package/docs/update-documentation.md +113 -0
- package/package.json +43 -0
- package/publish.log +93 -0
- package/quick-reference.md +169 -0
- package/src/admin/AdminAuthService.ts +88 -0
- package/src/admin/AdminSessionService.ts +91 -0
- package/src/admin/MetricsService.ts +145 -0
- package/src/admin/index.ts +13 -0
- package/src/backend/BackendMetadataAdapter.ts +88 -0
- package/src/backend/factory.ts +107 -0
- package/src/backend/index.ts +12 -0
- package/src/backend/medusa/MedusaMetadataAdapter.ts +139 -0
- package/src/core/errors.js +161 -0
- package/src/core/errors.ts +132 -0
- package/src/core/fetch-utils.js +240 -0
- package/src/core/fetch-utils.ts +227 -0
- package/src/core/index.ts +8 -0
- package/src/core/logger.js +63 -0
- package/src/core/logger.ts +37 -0
- package/src/factory/index.ts +102 -0
- package/src/index.ts +205 -0
- package/src/lib/money-transform.ts +80 -0
- package/src/metadata/channel-scope-validation.js +136 -0
- package/src/metadata/channel-scope-validation.ts +151 -0
- package/src/metadata/channel-scope.js +174 -0
- package/src/metadata/channel-scope.ts +215 -0
- package/src/metadata/index.js +31 -0
- package/src/metadata/index.ts +37 -0
- package/src/providers/bigcommerce/bigcommerce.ts +6 -0
- package/src/providers/bigcommerce/index.ts +315 -0
- package/src/providers/bigcommerce/transforms.ts +227 -0
- package/src/providers/index.ts +22 -0
- package/src/providers/magento/index.ts +387 -0
- package/src/providers/magento/magento.ts +5 -0
- package/src/providers/magento/queries.ts +267 -0
- package/src/providers/magento/transforms.ts +249 -0
- package/src/providers/medusa/auth.js +601 -0
- package/src/providers/medusa/auth.ts +496 -0
- package/src/providers/medusa/fulfillment-planner.ts +225 -0
- package/src/providers/medusa/index.js +2485 -0
- package/src/providers/medusa/index.ts +4239 -0
- package/src/providers/medusa/medusa.ts +8 -0
- package/src/providers/medusa/payment.js +265 -0
- package/src/providers/medusa/payment.ts +269 -0
- package/src/providers/medusa/transforms.js +505 -0
- package/src/providers/medusa/transforms.ts +625 -0
- package/src/providers/shopify/index.ts +444 -0
- package/src/providers/shopify/mutations.ts +102 -0
- package/src/providers/shopify/queries.ts +324 -0
- package/src/providers/shopify/shopify.ts +7 -0
- package/src/providers/shopify/transforms.ts +270 -0
- package/src/providers/spree/index.ts +344 -0
- package/src/providers/spree/spree.ts +5 -0
- package/src/providers/spree/transforms.ts +254 -0
- package/src/providers/woocommerce/index.ts +310 -0
- package/src/providers/woocommerce/transforms.ts +254 -0
- package/src/providers/woocommerce/woocommerce.ts +5 -0
- package/src/resilience/README.md +299 -0
- package/src/resilience/circuit-breaker.ts +222 -0
- package/src/resilience/index.ts +10 -0
- package/src/types/capabilities.types.js +141 -0
- package/src/types/capabilities.types.ts +366 -0
- package/src/types/index.ts +24 -0
- package/src/types/provider.js +9 -0
- package/src/types/provider.ts +9 -0
- package/src/types/ui-responses.ts +261 -0
- package/src/utils/city-coordinates.ts +40 -0
- package/src/utils/geolocation.ts +52 -0
- package/src/utils/payment-utils.js +152 -0
- package/src/utils/payment-utils.ts +203 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +14 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Resumen Ejecutivo - Solución a Errores de Fetch
|
|
2
|
+
|
|
3
|
+
## ❌ Problema
|
|
4
|
+
Errores frecuentes `Failed to fetch products: fetch failed` en el V0 starter al cargar productos desde el backend de Medusa.
|
|
5
|
+
|
|
6
|
+
## ✅ Solución Implementada
|
|
7
|
+
|
|
8
|
+
### 1. Nuevo Módulo de Retry Logic (`fetch-utils.ts`)
|
|
9
|
+
- **Ubicación**: `packages/adapters/src/core/fetch-utils.ts`
|
|
10
|
+
- **Funcionalidad**:
|
|
11
|
+
- Reintentos automáticos con exponential backoff
|
|
12
|
+
- Timeout configurable (10s por defecto)
|
|
13
|
+
- Detección inteligente de errores retryables
|
|
14
|
+
- Logging detallado
|
|
15
|
+
|
|
16
|
+
### 2. Integración en MedusaProvider
|
|
17
|
+
- **Ubicación**: `packages/adapters/src/providers/medusa/index.ts`
|
|
18
|
+
- **Cambios**:
|
|
19
|
+
- Nueva propiedad `retry?: RetryConfig` en `MedusaProviderConfig`
|
|
20
|
+
- Método privado `withRetry()` que envuelve todas las llamadas al SDK
|
|
21
|
+
- Métodos `getProduct()` y `getProducts()` actualizados para usar retry logic
|
|
22
|
+
|
|
23
|
+
### 3. Configuración por Defecto en V0 Starter
|
|
24
|
+
- **Ubicación**: `packages/core/lib/commerce.ts`
|
|
25
|
+
- **Configuración**:
|
|
26
|
+
```typescript
|
|
27
|
+
retry: {
|
|
28
|
+
maxRetries: 3, // 4 intentos totales
|
|
29
|
+
timeoutMs: 10000, // 10 segundos por request
|
|
30
|
+
initialDelayMs: 300, // Delay inicial 300ms
|
|
31
|
+
backoffMultiplier: 2, // Exponential backoff (2x)
|
|
32
|
+
maxDelayMs: 5000, // Cap en 5 segundos
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 📊 Impacto Esperado
|
|
37
|
+
- **90-98% reducción** en errores visibles al usuario
|
|
38
|
+
- Resiliencia ante problemas de red transitorios
|
|
39
|
+
- Mejor experiencia de usuario sin interrupciones
|
|
40
|
+
|
|
41
|
+
## 🧪 Estado
|
|
42
|
+
- ✅ Código compila sin errores TypeScript
|
|
43
|
+
- ✅ Build de adapters exitoso
|
|
44
|
+
- ✅ Build de elements exitoso
|
|
45
|
+
- ✅ Documentación completa creada
|
|
46
|
+
- 🔄 Pendiente: Testing en dev/staging
|
|
47
|
+
|
|
48
|
+
## 📝 Documentación Completa
|
|
49
|
+
Ver [fetch-reliability-solution.md](./fetch-reliability-solution.md) para análisis detallado de causas, soluciones, y configuraciones avanzadas.
|
|
50
|
+
|
|
51
|
+
## 🚀 Next Steps
|
|
52
|
+
1. Probar en desarrollo: `yarn dev:showcase` o `yarn workspace @thorprovider/core dev`
|
|
53
|
+
2. Verificar logs en consola mostrando reintentos
|
|
54
|
+
3. Confirmar reducción de errores en producción
|
|
55
|
+
4. (Opcional) Ajustar configuración de retry según métricas reales
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
type: how-to
|
|
5
|
+
tags: [auth, security, login, jwt, session, registration]
|
|
6
|
+
related:
|
|
7
|
+
- profile-management.md
|
|
8
|
+
- services-overview.md
|
|
9
|
+
- getting-started.md
|
|
10
|
+
updated: 2026-02-28
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Complete guide to setting up and using authentication with Thor adapters.
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- [Thor Adapters installed](getting-started.md#installation)
|
|
18
|
+
- A Medusa.js v2 backend configured with authentication providers
|
|
19
|
+
|
|
20
|
+
## Quick Example
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { commerce } from '@/lib/commerce';
|
|
24
|
+
|
|
25
|
+
// Simple login
|
|
26
|
+
const { customer } = await commerce.auth.login({
|
|
27
|
+
email: 'user@example.com',
|
|
28
|
+
password: 'SecurePassword123!',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
console.log(`Welcome back, ${customer.first_name}!`);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Step-by-Step Instructions
|
|
35
|
+
|
|
36
|
+
### Step 1: Configure Auth Method
|
|
37
|
+
|
|
38
|
+
Choose between JWT (SPAs) or Session (SSR) in your provider configuration.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// lib/commerce.ts
|
|
42
|
+
export const commerce = createProvider({
|
|
43
|
+
provider: 'medusa',
|
|
44
|
+
config: {
|
|
45
|
+
// ... basic config
|
|
46
|
+
auth: {
|
|
47
|
+
method: 'jwt', // Recommended for SPAs
|
|
48
|
+
storage: 'localStorage', // Where to persist tokens
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Step 2: Handle Registration
|
|
55
|
+
|
|
56
|
+
Capture user data and create a new customer account.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const { customer } = await commerce.auth.register({
|
|
60
|
+
email: 'new@example.com',
|
|
61
|
+
password: 'Password123!',
|
|
62
|
+
first_name: 'Jane',
|
|
63
|
+
last_name: 'Doe',
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Step 3: Implement Login Flow
|
|
68
|
+
|
|
69
|
+
Create a login handler in your UI components.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
async function handleLogin(credentials) {
|
|
73
|
+
try {
|
|
74
|
+
await commerce.auth.login(credentials);
|
|
75
|
+
// Redirect on success
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// Handle AuthError
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Step 4: Check Auth State
|
|
83
|
+
|
|
84
|
+
Protect routes or update UI based on authentication status.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const isAuth = await commerce.auth.isAuthenticated();
|
|
88
|
+
const customer = await commerce.auth.getCurrentCustomer();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## ✅ DO's
|
|
92
|
+
|
|
93
|
+
1. **Use JWT for Headless SPAs:** It's more flexible and works better across different origins.
|
|
94
|
+
2. **Use Session for SSR:** If you use Next.js Server Components, Session cookies are more secure.
|
|
95
|
+
3. **Handle AuthErrors specifically:** Use `instanceof AuthError` to show user-friendly messages.
|
|
96
|
+
4. **Enable Auto-refresh:** Keep `autoRefresh: true` (default) to ensure seamless user sessions.
|
|
97
|
+
5. **Clear local state on logout:** Always call `commerce.auth.logout()` to clear stored tokens.
|
|
98
|
+
|
|
99
|
+
## ❌ DONT's
|
|
100
|
+
|
|
101
|
+
1. **Don't store sensitive data in customer object:** Only use public profile fields.
|
|
102
|
+
2. **Don't handle JWT tokens manually:** Let the adapter manage storage and refresh headers.
|
|
103
|
+
3. **Don't use Secret Keys in the browser:** Authentication should only use Publishable Keys.
|
|
104
|
+
4. **Don't ignore the `isAuthenticated` check:** Verify auth status before performing protected operations.
|
|
105
|
+
5. **Don't hardcode login URLs:** The adapter knows where to send requests based on the provider.
|
|
106
|
+
|
|
107
|
+
## Common Patterns
|
|
108
|
+
|
|
109
|
+
### Protected Route Hook (React)
|
|
110
|
+
```typescript
|
|
111
|
+
function useRequireAuth() {
|
|
112
|
+
const { customer, loading } = useAuth(); // custom hook
|
|
113
|
+
const router = useRouter();
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!loading && !customer) router.push('/login');
|
|
117
|
+
}, [customer, loading]);
|
|
118
|
+
|
|
119
|
+
return { customer, loading };
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Server-Side Auth Check (Next.js)
|
|
124
|
+
```typescript
|
|
125
|
+
export async function getServerSideProps() {
|
|
126
|
+
const customer = await commerce.auth.getCurrentCustomer();
|
|
127
|
+
if (!customer) return { redirect: { destination: '/login' } };
|
|
128
|
+
return { props: { customer } };
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Common Errors & Solutions
|
|
133
|
+
|
|
134
|
+
### Error: `AUTH_FAILED`
|
|
135
|
+
**Cause:** Incorrect email or password.
|
|
136
|
+
**Solution:** Show "Invalid credentials" message to the user.
|
|
137
|
+
|
|
138
|
+
### Error: `TOKEN_EXPIRED`
|
|
139
|
+
**Cause:** Refresh token expired or user was logged out elsewhere.
|
|
140
|
+
**Solution:** Redirect to login page to re-authenticate.
|
|
141
|
+
|
|
142
|
+
### Error: `USER_NOT_FOUND`
|
|
143
|
+
**Cause:** Attempting to login with a non-existent email.
|
|
144
|
+
**Solution:** Prompt user to register if they don't have an account.
|
|
145
|
+
|
|
146
|
+
## Common Pitfalls
|
|
147
|
+
|
|
148
|
+
> ⚠️ **Pitfall 1: CORS issues with Session cookies**
|
|
149
|
+
> Session-based auth requires matching domains or properly configured `cross-site` cookies in the backend.
|
|
150
|
+
|
|
151
|
+
> ⚠️ **Pitfall 2: LocalStorage availability in SSR**
|
|
152
|
+
> Ensure token management logic checks for `window` availability if using `localStorage`.
|
|
153
|
+
|
|
154
|
+
## Troubleshooting
|
|
155
|
+
|
|
156
|
+
| Issue | Solution |
|
|
157
|
+
|-------|----------|
|
|
158
|
+
| Token not sent in headers | Check if `auth.method` matches backend expectation |
|
|
159
|
+
| Login works but profile fails | Verify `storage` type is correctly configured and working |
|
|
160
|
+
| Infinite login loop | Check cookie path and domain settings on the backend |
|
|
161
|
+
|
|
162
|
+
## Next Steps
|
|
163
|
+
|
|
164
|
+
- [Profile Management](profile-management.md)
|
|
165
|
+
- [Services Overview](services-overview.md)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Capabilities System
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
type: reference
|
|
5
|
+
tags: [architecture, capabilities, backend, detection]
|
|
6
|
+
related:
|
|
7
|
+
- providers-overview.md
|
|
8
|
+
- services-overview.md
|
|
9
|
+
updated: 2026-02-28
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
The capabilities system exposes which operations each backend provider supports, enabling adaptive UIs across platforms.
|
|
13
|
+
|
|
14
|
+
## Quick Reference
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { commerce } from '@/lib/commerce';
|
|
18
|
+
|
|
19
|
+
// Check any capability
|
|
20
|
+
if (commerce.capabilities.updateEmail) {
|
|
21
|
+
// Safe to call
|
|
22
|
+
await commerce.auth.updateCustomer({ email: 'new@email.com' });
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Available Capabilities
|
|
27
|
+
|
|
28
|
+
### Profile Operations
|
|
29
|
+
|
|
30
|
+
| Capability | Type | Description |
|
|
31
|
+
|------------|------|-------------|
|
|
32
|
+
| `updateProfile` | `boolean` | Can update name, phone, etc. |
|
|
33
|
+
| `updateEmail` | `boolean` | Can change the primary email address. |
|
|
34
|
+
| `changePasswordDirect` | `boolean` | Supports `password` field in `updateCustomer`. |
|
|
35
|
+
| `changePasswordViaEmail` | `boolean` | Supports `requestPasswordReset` method. |
|
|
36
|
+
| `deleteAccount` | `boolean` | Supports `deleteAccount` method. |
|
|
37
|
+
| `uploadAvatar` | `boolean` | Supports native file upload for avatars. |
|
|
38
|
+
|
|
39
|
+
### Cart Operations
|
|
40
|
+
|
|
41
|
+
| Capability | Type | Description |
|
|
42
|
+
|------------|------|-------------|
|
|
43
|
+
| `cartPromoCode` | `boolean` | Can apply discount/promo codes to cart. |
|
|
44
|
+
| `cartGiftCards` | `boolean` | Supports gift card application. |
|
|
45
|
+
| `cartNotes` | `boolean` | Supports order/cart notes. |
|
|
46
|
+
| `cartCustomAttributes` | `boolean` | Supports metadata/attributes at cart level. |
|
|
47
|
+
|
|
48
|
+
### Product & Discovery
|
|
49
|
+
|
|
50
|
+
| Capability | Type | Description |
|
|
51
|
+
|------------|------|-------------|
|
|
52
|
+
| `productVariants` | `boolean` | Backend supports multiple variants per product. |
|
|
53
|
+
| `productCategories` | `boolean` | Supports nested categories or collections. |
|
|
54
|
+
| `productInventory` | `boolean` | Real-time inventory tracking available. |
|
|
55
|
+
| `productDigital` | `boolean` | Supports digital/downloadable products. |
|
|
56
|
+
| `productReviews` | `boolean` | Native review system support. |
|
|
57
|
+
|
|
58
|
+
## Type Definitions
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
export interface BackendCapabilities {
|
|
62
|
+
// Profile
|
|
63
|
+
updateProfile: boolean;
|
|
64
|
+
updateEmail: boolean;
|
|
65
|
+
changePasswordDirect: boolean;
|
|
66
|
+
changePasswordViaEmail: boolean;
|
|
67
|
+
deleteAccount: boolean;
|
|
68
|
+
uploadAvatar: boolean;
|
|
69
|
+
|
|
70
|
+
// Avatar Extensions
|
|
71
|
+
avatarViaMetadata: boolean;
|
|
72
|
+
|
|
73
|
+
// Cart
|
|
74
|
+
cartPromoCode: boolean;
|
|
75
|
+
cartGiftCards: boolean;
|
|
76
|
+
cartNotes: boolean;
|
|
77
|
+
cartCustomAttributes: boolean;
|
|
78
|
+
|
|
79
|
+
// Products
|
|
80
|
+
productVariants: boolean;
|
|
81
|
+
productCategories: boolean;
|
|
82
|
+
productInventory: boolean;
|
|
83
|
+
productDigital: boolean;
|
|
84
|
+
productReviews: boolean;
|
|
85
|
+
|
|
86
|
+
// Auth Integration
|
|
87
|
+
authJWT: boolean;
|
|
88
|
+
authSession: boolean;
|
|
89
|
+
authOAuth: boolean;
|
|
90
|
+
authPasswordless: boolean;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Examples
|
|
95
|
+
|
|
96
|
+
### Basic: Conditional UI
|
|
97
|
+
```tsx
|
|
98
|
+
const { canDelete } = useCapabilities();
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Button disabled={!canDelete} color="red">
|
|
102
|
+
Delete Account
|
|
103
|
+
</Button>
|
|
104
|
+
);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Advanced: Multi-provider Fallback
|
|
108
|
+
```typescript
|
|
109
|
+
async function handleAvatarUpdate(file: File) {
|
|
110
|
+
if (caps.uploadAvatar) {
|
|
111
|
+
return await commerce.auth.uploadAvatar(file);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (caps.avatarViaMetadata) {
|
|
115
|
+
const url = await externalUpload(file);
|
|
116
|
+
return await commerce.auth.updateCustomer({
|
|
117
|
+
metadata: { avatar_url: url }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw new Error('Avatar updates not supported on this backend');
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Edge Cases
|
|
126
|
+
|
|
127
|
+
- **Runtime Changes:** Capabilities are static per provider instance. If a backend configuration changes, the provider must be re-initialized.
|
|
128
|
+
- **Partial Support:** A capability might be `true` but only for specific users. Local validation still applies.
|
|
129
|
+
- **Provider Switching:** When switching providers (e.g., Medusa to Shopify), ensure the UI handles the change in capabilities instantly.
|
|
130
|
+
|
|
131
|
+
## Performance
|
|
132
|
+
|
|
133
|
+
Capabilities are calculated once during provider initialization. Accessing `commerce.capabilities` is a zero-latency O(1) operation.
|
|
134
|
+
|
|
135
|
+
## Accessibility
|
|
136
|
+
|
|
137
|
+
- **Disabled States:** When a capability is `false`, ensure inputs are correctly marked as `disabled` and provide an `aria-description` explaining why.
|
|
138
|
+
- **Dynamic Content:** Use `aria-live` regions if UI sections disappear based on backend capabilities during initialization.
|
|
139
|
+
|
|
140
|
+
## See Also
|
|
141
|
+
|
|
142
|
+
- [Providers Overview](providers-overview.md)
|
|
143
|
+
- [Services Overview](services-overview.md)
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Category Customization & Inheritance
|
|
2
|
+
|
|
3
|
+
## The Problem (Simple Version)
|
|
4
|
+
|
|
5
|
+
Your store sells **only shoes**. But the backend (Medusa) gives you 10 categories: "Formales", "Deportivos", "Sandalias", "Accesorios", etc.
|
|
6
|
+
|
|
7
|
+
**Question**: What do you do with categories you don't use?
|
|
8
|
+
|
|
9
|
+
### Option A: Take It Or Leave It (Bad)
|
|
10
|
+
- Accept all 10 categories as-is
|
|
11
|
+
- Can't hide unwanted ones
|
|
12
|
+
- Can't rename for your market (Spain: "fundas" vs USA: "covers")
|
|
13
|
+
- **Result**: Broken UX, confused customers
|
|
14
|
+
|
|
15
|
+
### Option B: Personalize Inheritance (Good ✨)
|
|
16
|
+
- Get all categories from backend
|
|
17
|
+
- But you can:
|
|
18
|
+
- **Hide** ones you don't need
|
|
19
|
+
- **Rename** to match your market
|
|
20
|
+
- **Add new** custom categories if Medusa doesn't have them
|
|
21
|
+
- **Toggle** inheritance on/off
|
|
22
|
+
|
|
23
|
+
**Result**: Flexible, multi-market ready, no backend changes needed.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Real-World Example
|
|
28
|
+
|
|
29
|
+
### Scenario: "Zapatos Cubanos Online"
|
|
30
|
+
|
|
31
|
+
**Backend (Medusa) provides:**
|
|
32
|
+
```
|
|
33
|
+
- Formales
|
|
34
|
+
- Deportivos
|
|
35
|
+
- Sandalias
|
|
36
|
+
- Accesorios
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**You customize in dashboard:**
|
|
40
|
+
```
|
|
41
|
+
❌ Hide "Sandalias" (you don't sell sandals)
|
|
42
|
+
✏️ Rename "Accesorios" → "Extras"
|
|
43
|
+
✨ Add "Bestsellers" (custom category)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Customer sees in storefront:**
|
|
47
|
+
```
|
|
48
|
+
- Formales
|
|
49
|
+
- Deportivos
|
|
50
|
+
- Extras (renamed)
|
|
51
|
+
- Bestsellers (custom)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Database stores (in metadata):**
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"categoryCustomizations": {
|
|
58
|
+
"hidden": ["sandalias"],
|
|
59
|
+
"renamed": { "accesorios": "Extras" },
|
|
60
|
+
"custom": [
|
|
61
|
+
{ "id": "bestsellers", "name": "Bestsellers", ... }
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## How It Works (The 5-Phase System)
|
|
70
|
+
|
|
71
|
+
### Phase 1: Define Vocabulary (L1 — Types)
|
|
72
|
+
```
|
|
73
|
+
What is a customization?
|
|
74
|
+
→ { hidden: ["id1", "id2"], renamed: {id: "newname"}, custom: [{...}] }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
We define this **once** in `@thorprovider/types` so every layer speaks the same language.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### Phase 2: Store in Backend (L2 — Metadata Adapter)
|
|
82
|
+
```
|
|
83
|
+
Backend (Medusa, Shopify, etc.)
|
|
84
|
+
↓
|
|
85
|
+
BackendMetadataAdapter
|
|
86
|
+
↓ (reads/writes JSON in sales_channel.metadata)
|
|
87
|
+
Your customizations
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Key principle**: We use the backend's native **metadata storage**, not a separate database.
|
|
91
|
+
|
|
92
|
+
Why?
|
|
93
|
+
- **Simple**: No external SQLite/Supabase
|
|
94
|
+
- **Multi-tenant ready**: Each sales channel has its own metadata
|
|
95
|
+
- **Agnóstic**: Medusa, Shopify, WordPress — same interface
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### Phase 3: Merge Logic (L2 — Service)
|
|
100
|
+
```
|
|
101
|
+
Input:
|
|
102
|
+
baseCategories = [Formales, Deportivos, Sandalias]
|
|
103
|
+
customizations = { hidden: [Sandalias], renamed: {...}, custom: [...] }
|
|
104
|
+
|
|
105
|
+
Function mergeCategories(base, customizations):
|
|
106
|
+
1. Remove hidden categories
|
|
107
|
+
2. Rename categories per config
|
|
108
|
+
3. Add custom categories
|
|
109
|
+
4. Return final list
|
|
110
|
+
|
|
111
|
+
Output:
|
|
112
|
+
effectiveCategories = [Formales, Deportivos, Extras, Bestsellers]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Why L2?** This is business logic (not UI, not type definition). It belongs with the adapter.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### Phase 4: Expose via API (L5 — Route Handler)
|
|
120
|
+
```
|
|
121
|
+
GET /api/admin/categories
|
|
122
|
+
→ Returns merged categories (dashboard reads state)
|
|
123
|
+
|
|
124
|
+
POST /api/admin/categories
|
|
125
|
+
→ Dashboard sends new customizations, backend saves them
|
|
126
|
+
|
|
127
|
+
PUT /api/admin/categories
|
|
128
|
+
→ (Optional) Update a single category (hide/rename)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Why L5?** Only the app knows HTTP. Only the app can validate requests.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### Phase 5: Dashboard UI (L4 — Component)
|
|
136
|
+
```
|
|
137
|
+
┌─────────────────────────────────┐
|
|
138
|
+
│ ⚙️ Manage Categories │
|
|
139
|
+
├─────────────────────────────────┤
|
|
140
|
+
│ ☑️ Use inherited categories │ ← Toggle
|
|
141
|
+
├─────────────────────────────────┤
|
|
142
|
+
│ Base Categories: │
|
|
143
|
+
│ ☑️ Formales │
|
|
144
|
+
│ ☑️ Deportivos │
|
|
145
|
+
│ ☐ Sandalias [HIDDEN] │
|
|
146
|
+
│ (Extras) ← Renamed from "Accesorios"
|
|
147
|
+
├─────────────────────────────────┤
|
|
148
|
+
│ Custom Categories: │
|
|
149
|
+
│ ✨ Bestsellers │
|
|
150
|
+
│ [Edit] [Delete] │
|
|
151
|
+
├─────────────────────────────────┤
|
|
152
|
+
│ │
|
|
153
|
+
│ [+ Add Custom] [Save] │
|
|
154
|
+
└─────────────────────────────────┘
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Why L4?** Reusable component (could work in different apps). Doesn't know about HTTP/databases.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Why This Architecture Is "Agnóstic"
|
|
162
|
+
|
|
163
|
+
**Agnóstic** = "Works with any backend, no vendor lock-in."
|
|
164
|
+
|
|
165
|
+
### Data Flow
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
┌─────────────────────┐
|
|
169
|
+
│ Medusa │ (or Shopify, WordPress)
|
|
170
|
+
│ sales_channel. │
|
|
171
|
+
│ metadata │
|
|
172
|
+
└──────────┬──────────┘
|
|
173
|
+
│ (Phase 2: BackendMetadataAdapter)
|
|
174
|
+
↓
|
|
175
|
+
┌──────────────────────────────────────┐
|
|
176
|
+
│ BackendMetadataAdapter (Interface)│
|
|
177
|
+
│ - getMetadata() │
|
|
178
|
+
│ - setMetadata() │
|
|
179
|
+
│ (Same methods for all backends) │
|
|
180
|
+
└──────────┬───────────────────────────┘
|
|
181
|
+
│ (Phase 3: mergeCategories)
|
|
182
|
+
↓
|
|
183
|
+
┌──────────────────────────────────────┐
|
|
184
|
+
│ CategoryService │
|
|
185
|
+
│ mergeCategories(base, custom) │
|
|
186
|
+
│ (Pure function, no dependencies) │
|
|
187
|
+
└──────────┬───────────────────────────┘
|
|
188
|
+
│ (Phase 4: API exposes)
|
|
189
|
+
↓
|
|
190
|
+
┌──────────────────────────────────────┐
|
|
191
|
+
│ /api/admin/categories │
|
|
192
|
+
│ (GET merged, POST customizations) │
|
|
193
|
+
└──────────┬───────────────────────────┘
|
|
194
|
+
│ (Phase 5: UI consumes)
|
|
195
|
+
↓
|
|
196
|
+
┌──────────────────────────────────────┐
|
|
197
|
+
│ CategoryCustomization Component │
|
|
198
|
+
│ (Dashboard controls) │
|
|
199
|
+
└──────────────────────────────────────┘
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### If You Change Backend: Medusa → Shopify
|
|
203
|
+
|
|
204
|
+
**What changes:**
|
|
205
|
+
- Phase 2 implementation (MedusaMetadataAdapter → ShopifyMetadataAdapter)
|
|
206
|
+
|
|
207
|
+
**What stays the same:**
|
|
208
|
+
- Phase 1 (types)
|
|
209
|
+
- Phase 3 (mergeCategories function)
|
|
210
|
+
- Phase 4 (API route)
|
|
211
|
+
- Phase 5 (UI component)
|
|
212
|
+
|
|
213
|
+
**Why?** All non-Phase-2 code depends on the **interface**, not the **implementation**.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Key Concepts
|
|
218
|
+
|
|
219
|
+
### 1. Inheritance = Default Categories
|
|
220
|
+
|
|
221
|
+
The backend provides a default set (e.g., Medusa categories). You inherit them by default, but can modify.
|
|
222
|
+
|
|
223
|
+
### 2. Customization = Your Changes
|
|
224
|
+
|
|
225
|
+
Your modifications (hidden, renamed, custom) are stored **independently** from base categories.
|
|
226
|
+
|
|
227
|
+
This way:
|
|
228
|
+
- Base categories can update from Medusa (new image, new subcategories)
|
|
229
|
+
- Your customizations persist and reapply on top
|
|
230
|
+
|
|
231
|
+
### 3. Effective Categories = Result
|
|
232
|
+
|
|
233
|
+
What the storefront **actually sees** = base + customizations merged.
|
|
234
|
+
|
|
235
|
+
The storefront never knows about inheritance. It just fetches categories and displays them.
|
|
236
|
+
|
|
237
|
+
### 4. Metadata Storage = No Extra Database
|
|
238
|
+
|
|
239
|
+
We use the backend's **metadata** field (exists in Medusa, Shopify, etc.).
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
Medusa: sales_channel.metadata = { category_customizations: {...} }
|
|
243
|
+
Shopify: custom field or app data store (same interface)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Result: One less database to maintain.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Implementation Order (For Next Agent)
|
|
251
|
+
|
|
252
|
+
See `.development-guides/category-customization-implementation.md` for detailed step-by-step.
|
|
253
|
+
|
|
254
|
+
**TL;DR:**
|
|
255
|
+
1. Add types in L1
|
|
256
|
+
2. Extend metadata adapter in L2
|
|
257
|
+
3. Create `mergeCategories()` service in L2
|
|
258
|
+
4. Integrate into `CommerceProvider.getCategories()` in L2
|
|
259
|
+
5. Create API route in L5
|
|
260
|
+
6. Create UI component in L4
|
|
261
|
+
7. Integrate into dashboard in L5
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## FAQ
|
|
266
|
+
|
|
267
|
+
### Q: Can I hide a category if customers already have it in their cart?
|
|
268
|
+
|
|
269
|
+
**A**: Yes, but consider adding a migration script:
|
|
270
|
+
- Categories hidden after purchase → show warning on checkout
|
|
271
|
+
- Don't render "orphan" categories in storefront
|
|
272
|
+
- Backend (Medusa) still serves the products (they exist, just not categorized)
|
|
273
|
+
|
|
274
|
+
### Q: What if I rename a category but Medusa updates the base category name?
|
|
275
|
+
|
|
276
|
+
**A**: The renamed version wins. Custom > Base.
|
|
277
|
+
|
|
278
|
+
If you want to un-rename, just delete the entry from the `renamed` map.
|
|
279
|
+
|
|
280
|
+
### Q: Can I have nested custom categories?
|
|
281
|
+
|
|
282
|
+
**A**: Yes, use `parentCategoryId` in custom category objects.
|
|
283
|
+
|
|
284
|
+
But ensure your storefront supports hierarchical rendering.
|
|
285
|
+
|
|
286
|
+
### Q: What happens if I toggle inheritance off?
|
|
287
|
+
|
|
288
|
+
**A**: Only custom categories show. Base categories are ignored (but not deleted).
|
|
289
|
+
|
|
290
|
+
If you toggle back on, base categories reappear (minus any you've hidden).
|
|
291
|
+
|
|
292
|
+
### Q: How do I add an image to a custom category?
|
|
293
|
+
|
|
294
|
+
**A**: Custom categories support `metadata: { image: "..." }`.
|
|
295
|
+
|
|
296
|
+
Storefront should render it like base categories.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Related Files
|
|
301
|
+
|
|
302
|
+
- **Implementation plan**: `.development-guides/category-customization-implementation.md`
|
|
303
|
+
- **Types**: `packages/types/src/admin/category-customizations.ts`
|
|
304
|
+
- **Metadata adapter**: `packages/adapters/src/backend/BackendMetadataAdapter.ts`
|
|
305
|
+
- **Service**: `packages/adapters/src/services/category-service.ts`
|
|
306
|
+
- **Provider integration**: `packages/adapters/src/providers/medusa/index.ts` (getCategories)
|
|
307
|
+
- **API route**: `packages/core/app/api/admin/categories/route.ts`
|
|
308
|
+
- **Component**: `packages/components/src/patterns/modules/dashboard/CategoryCustomization.tsx`
|
|
309
|
+
|