@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.
Files changed (95) hide show
  1. package/.turbo/turbo-build.log +18 -0
  2. package/.turbo/turbo-test.log +1 -0
  3. package/.turbo/turbo-type-check.log +0 -0
  4. package/AGENTS.md +1 -0
  5. package/CHANGELOG.md +271 -0
  6. package/README.md +634 -0
  7. package/dist/.tsbuildinfo +1 -0
  8. package/dist/index.d.mts +2524 -0
  9. package/dist/index.d.ts +2524 -0
  10. package/dist/index.js +8136 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/index.mjs +8045 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/docs/RETRY_SOLUTION_SUMMARY.md +55 -0
  15. package/docs/authentication.md +165 -0
  16. package/docs/capabilities-system.md +143 -0
  17. package/docs/category-customization.md +309 -0
  18. package/docs/fetch-reliability-solution.md +399 -0
  19. package/docs/getting-started.md +154 -0
  20. package/docs/multi-tenant-channel-scoping.md +225 -0
  21. package/docs/profile-management.md +147 -0
  22. package/docs/providers-overview.md +85 -0
  23. package/docs/services-overview.md +135 -0
  24. package/docs/update-documentation.md +113 -0
  25. package/package.json +43 -0
  26. package/publish.log +93 -0
  27. package/quick-reference.md +169 -0
  28. package/src/admin/AdminAuthService.ts +88 -0
  29. package/src/admin/AdminSessionService.ts +91 -0
  30. package/src/admin/MetricsService.ts +145 -0
  31. package/src/admin/index.ts +13 -0
  32. package/src/backend/BackendMetadataAdapter.ts +88 -0
  33. package/src/backend/factory.ts +107 -0
  34. package/src/backend/index.ts +12 -0
  35. package/src/backend/medusa/MedusaMetadataAdapter.ts +139 -0
  36. package/src/core/errors.js +161 -0
  37. package/src/core/errors.ts +132 -0
  38. package/src/core/fetch-utils.js +240 -0
  39. package/src/core/fetch-utils.ts +227 -0
  40. package/src/core/index.ts +8 -0
  41. package/src/core/logger.js +63 -0
  42. package/src/core/logger.ts +37 -0
  43. package/src/factory/index.ts +102 -0
  44. package/src/index.ts +205 -0
  45. package/src/lib/money-transform.ts +80 -0
  46. package/src/metadata/channel-scope-validation.js +136 -0
  47. package/src/metadata/channel-scope-validation.ts +151 -0
  48. package/src/metadata/channel-scope.js +174 -0
  49. package/src/metadata/channel-scope.ts +215 -0
  50. package/src/metadata/index.js +31 -0
  51. package/src/metadata/index.ts +37 -0
  52. package/src/providers/bigcommerce/bigcommerce.ts +6 -0
  53. package/src/providers/bigcommerce/index.ts +315 -0
  54. package/src/providers/bigcommerce/transforms.ts +227 -0
  55. package/src/providers/index.ts +22 -0
  56. package/src/providers/magento/index.ts +387 -0
  57. package/src/providers/magento/magento.ts +5 -0
  58. package/src/providers/magento/queries.ts +267 -0
  59. package/src/providers/magento/transforms.ts +249 -0
  60. package/src/providers/medusa/auth.js +601 -0
  61. package/src/providers/medusa/auth.ts +496 -0
  62. package/src/providers/medusa/fulfillment-planner.ts +225 -0
  63. package/src/providers/medusa/index.js +2485 -0
  64. package/src/providers/medusa/index.ts +4239 -0
  65. package/src/providers/medusa/medusa.ts +8 -0
  66. package/src/providers/medusa/payment.js +265 -0
  67. package/src/providers/medusa/payment.ts +269 -0
  68. package/src/providers/medusa/transforms.js +505 -0
  69. package/src/providers/medusa/transforms.ts +625 -0
  70. package/src/providers/shopify/index.ts +444 -0
  71. package/src/providers/shopify/mutations.ts +102 -0
  72. package/src/providers/shopify/queries.ts +324 -0
  73. package/src/providers/shopify/shopify.ts +7 -0
  74. package/src/providers/shopify/transforms.ts +270 -0
  75. package/src/providers/spree/index.ts +344 -0
  76. package/src/providers/spree/spree.ts +5 -0
  77. package/src/providers/spree/transforms.ts +254 -0
  78. package/src/providers/woocommerce/index.ts +310 -0
  79. package/src/providers/woocommerce/transforms.ts +254 -0
  80. package/src/providers/woocommerce/woocommerce.ts +5 -0
  81. package/src/resilience/README.md +299 -0
  82. package/src/resilience/circuit-breaker.ts +222 -0
  83. package/src/resilience/index.ts +10 -0
  84. package/src/types/capabilities.types.js +141 -0
  85. package/src/types/capabilities.types.ts +366 -0
  86. package/src/types/index.ts +24 -0
  87. package/src/types/provider.js +9 -0
  88. package/src/types/provider.ts +9 -0
  89. package/src/types/ui-responses.ts +261 -0
  90. package/src/utils/city-coordinates.ts +40 -0
  91. package/src/utils/geolocation.ts +52 -0
  92. package/src/utils/payment-utils.js +152 -0
  93. package/src/utils/payment-utils.ts +203 -0
  94. package/tsconfig.json +15 -0
  95. package/tsup.config.ts +14 -0
@@ -0,0 +1,225 @@
1
+ # Multi-Tenant Channel Scoping
2
+
3
+ > Backend-agnostic pattern for isolating entities by sales channel using metadata.
4
+
5
+ ## Problem
6
+
7
+ Thor Commerce deploys one V0 Starter per sales channel (multi-tenant).
8
+ Products and carts are natively scoped by Medusa's `sales_channel_id`, but
9
+ **customers, orders, and categories are not**. A dropshipper must only see
10
+ their own customers — never those of another channel or the main store.
11
+
12
+ ## Solution: Metadata-Based Channel Scope
13
+
14
+ Tag un-scoped entities with a well-known metadata key:
15
+
16
+ ```
17
+ entity.metadata.sales_channel_id = "sc_01HXXX"
18
+ ```
19
+
20
+ Then filter at the adapter level (L2) before data reaches L3–L5.
21
+
22
+ ### Why metadata?
23
+
24
+ | Criteria | Metadata | Custom DB field |
25
+ |-----------------------|-------------------|-----------------------|
26
+ | Zero migrations | ✅ | ❌ per backend |
27
+ | Works across backends | ✅ (Medusa, Shopify metafields, WC meta) | ❌ vendor-specific |
28
+ | Writable via SDK | ✅ admin API | ❌ requires backend deploy |
29
+ | Type-safe | ⚠️ runtime validation | ✅ DB schema |
30
+ | Query-indexed | ❌ in-memory filter | ✅ DB index |
31
+
32
+ For most stores (<10k entities per type) in-memory filtering is acceptable.
33
+ For larger catalogs, consider adding a DB index on
34
+ `customer.metadata->>'sales_channel_id'` in your Medusa backend.
35
+
36
+ ## Architecture
37
+
38
+ ```
39
+ L1 (@thorprovider/types)
40
+ └─ Customer.metadata, Order.metadata, GetCustomersOptions.salesChannelId
41
+
42
+ L2 (@thorprovider/adapters)
43
+ ├─ metadata/channel-scope.ts ← Pure helpers (build, read, filter)
44
+ ├─ metadata/channel-scope-validation.ts ← Validation (no external deps)
45
+ ├─ backend/MedusaMetadataAdapter ← Read/write metadata per resource type
46
+ └─ providers/medusa/index.ts ← assignCustomerToChannel(), filterByChannel()
47
+
48
+ L5 (V0 Starter)
49
+ └─ API routes / server actions call assignCustomerToChannel() post-registration
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ### Assign a customer to a channel
55
+
56
+ ```typescript
57
+ // In L5 API route (post-registration)
58
+ const customer = await commerce.assignCustomerToChannel(
59
+ customerId,
60
+ process.env.NEXT_PUBLIC_SALES_CHANNEL_ID!,
61
+ 'system', // optional: who assigned
62
+ );
63
+ ```
64
+
65
+ ### List customers scoped to a channel
66
+
67
+ ```typescript
68
+ // Dropshipper view — only their customers
69
+ const customers = await commerce.getCustomers({
70
+ salesChannelId: process.env.NEXT_PUBLIC_SALES_CHANNEL_ID,
71
+ });
72
+
73
+ // Admin view — all customers
74
+ const allCustomers = await commerce.getCustomers();
75
+ ```
76
+
77
+ ### Assign an order to a channel
78
+
79
+ ```typescript
80
+ await commerce.assignOrderToChannel(
81
+ orderId,
82
+ process.env.NEXT_PUBLIC_SALES_CHANNEL_ID!,
83
+ 'checkout-flow',
84
+ );
85
+ ```
86
+
87
+ ### Use helpers directly (any entity)
88
+
89
+ ```typescript
90
+ import {
91
+ buildChannelScopeMetadata,
92
+ filterByChannel,
93
+ getChannelScope,
94
+ isScopedToChannel,
95
+ partitionByChannel,
96
+ } from '@thorprovider/adapters';
97
+
98
+ // Build metadata payload
99
+ const meta = buildChannelScopeMetadata('sc_123', 'admin@store.com');
100
+ // → { sales_channel_id: 'sc_123', channel_assigned_at: '2026-03-27T...', channel_assigned_by: 'admin@store.com' }
101
+
102
+ // Filter an array of entities
103
+ const myCustomers = filterByChannel(allCustomers, 'sc_123');
104
+
105
+ // Check one entity
106
+ if (isScopedToChannel(customer, 'sc_123')) { /* ... */ }
107
+
108
+ // Partition into scoped + global
109
+ const { scoped, global } = partitionByChannel(customers, 'sc_123');
110
+ ```
111
+
112
+ ### Validation
113
+
114
+ ```typescript
115
+ import {
116
+ validateChannelScopeAssignment,
117
+ assertEntityBelongsToChannel,
118
+ ChannelScopeValidationError,
119
+ } from '@thorprovider/adapters';
120
+
121
+ // Validates all inputs before assignment
122
+ validateChannelScopeAssignment(customerId, channelId, assignedBy);
123
+
124
+ // Guard: ensure entity belongs to caller's channel
125
+ try {
126
+ assertEntityBelongsToChannel(customer, myChannelId);
127
+ } catch (e) {
128
+ if (e instanceof ChannelScopeValidationError) {
129
+ return res.status(403).json({ error: 'Access denied' });
130
+ }
131
+ }
132
+ ```
133
+
134
+ ## Extending to New Entity Types
135
+
136
+ The pattern is generic. To scope a new entity type:
137
+
138
+ 1. **Ensure** the entity type in `@thorprovider/types` has `metadata?: Record<string, unknown>`
139
+ 2. **Add** the resource type to `MedusaMetadataAdapter` (`getMetadata` / `setMetadata`)
140
+ 3. **Add** a `salesChannelId` filter option to the entity's query options type
141
+ 4. **Call** `filterByChannel()` in the provider's list method
142
+ 5. **Optionally** add an `assignXToChannel()` convenience method
143
+
144
+ ### Currently supported
145
+
146
+ | Entity | Native scoping (Medusa) | Metadata scoping (Thor) |
147
+ |-----------------|------------------------|------------------------|
148
+ | Products | ✅ `sales_channel_id` | N/A (native) |
149
+ | Carts | ✅ `sales_channel_id` | N/A (native) |
150
+ | Categories | ❌ | ⚠️ Via product cross-ref |
151
+ | **Customers** | ❌ | ✅ `metadata.sales_channel_id` |
152
+ | **Orders** | ❌ | ✅ `metadata.sales_channel_id` |
153
+ | Collections | ❌ (linked via products) | Planned |
154
+
155
+ ## When to Assign Channel Scope
156
+
157
+ | Event | Action |
158
+ |----------------------------|---------------------------------------------|
159
+ | Customer registers | `assignCustomerToChannel()` post-registration |
160
+ | Dropshipper creates customer | `assignCustomerToChannel()` in admin flow |
161
+ | Order completed | `assignOrderToChannel()` in checkout flow |
162
+ | Admin reassigns entity | `assignXToChannel()` from admin UI |
163
+
164
+ ## Backend-Specific Notes
165
+
166
+ ### Medusa v2
167
+
168
+ - Uses `admin.customer.update(id, { metadata })` — requires admin JWT
169
+ - Uses `admin.order.update(id, { metadata })` — requires admin JWT
170
+ - Metadata is a JSONB column — no size limit in practice
171
+ - **Not indexed** by default; add a GIN index if filtering >10k entities
172
+
173
+ ### Shopify (planned)
174
+
175
+ - Would use Customer Metafields API
176
+ - Key: `thor_commerce.sales_channel_id`
177
+ - Namespace isolates Thor metadata from other apps
178
+
179
+ ### WooCommerce (planned)
180
+
181
+ - Would use `wp_usermeta` table
182
+ - Key: `_thor_sales_channel_id`
183
+ - Prefix `_` hides from admin UI by convention
184
+
185
+ ## Optional: Custom Fetcher Callbacks
186
+
187
+ For backends that implement custom scoping endpoints (e.g., Medusa Module Link or custom API routes),
188
+ use the optional `customFetcher` callback in query options:
189
+
190
+ ```typescript
191
+ // Signature: override default SDK logic with custom endpoint
192
+ const customFetcher = async (options: GetCustomersOptions) => {
193
+ const channelId = options.salesChannelId;
194
+ if (!channelId) throw new Error('Channel scoping required for custom fetcher');
195
+
196
+ // Call custom endpoint instead of SDK
197
+ const response = await fetch(`/api/admin/sales-channels/${channelId}/customers`);
198
+ return response.json();
199
+ };
200
+
201
+ // Usage in L5 (V0 Starter)
202
+ const customers = await commerce.getCustomers({
203
+ salesChannelId: 'sc_123',
204
+ customFetcher, // ← Uses callback instead of Medusa SDK
205
+ });
206
+ ```
207
+
208
+ ### Benefits
209
+
210
+ | Scenario | Without callback | With callback |
211
+ |----------|------------------|---------------|
212
+ | Medusa Module Link | Requires wrapper function in L5 | `customFetcher` passed at query time |
213
+ | Custom API endpoint | SDK → in-memory filter | Direct server-side filtering |
214
+ | Shopify (future) | Would need separate method | Uniform interface across backends |
215
+
216
+ The callback pattern defers implementation complexity while maintaining type safety.
217
+ Backends can adopt it independently without changing L3–L5 code.
218
+
219
+ ## Security Considerations
220
+
221
+ - Channel assignment requires **admin-level authentication**
222
+ - `getCustomers({ salesChannelId })` filters in-memory after fetch (or via callback if provided)
223
+ - Defense-in-depth: combine with JWT scope where available
224
+ - `assertEntityBelongsToChannel()` provides explicit ownership checks
225
+ - Custom fetchers should **validate `salesChannelId`** before calling backend
@@ -0,0 +1,147 @@
1
+ # Profile Management
2
+
3
+ ---
4
+ type: how-to
5
+ tags: [profile, account, customer, metadata, avatar]
6
+ related:
7
+ - authentication.md
8
+ - capabilities-system.md
9
+ - services-overview.md
10
+ updated: 2026-02-28
11
+ ---
12
+
13
+ Guide for managing customer profiles using capability-aware patterns for cross-backend compatibility.
14
+
15
+ ## Prerequisites
16
+
17
+ - [Authentication set up](authentication.md)
18
+ - User logged in to access profile features
19
+
20
+ ## Quick Example
21
+
22
+ ```typescript
23
+ import { commerce } from '@/lib/commerce';
24
+
25
+ // Simple profile update
26
+ const updated = await commerce.auth.updateCustomer({
27
+ first_name: 'Thor',
28
+ last_name: 'Developer',
29
+ });
30
+ ```
31
+
32
+ ## Step-by-Step Instructions
33
+
34
+ ### Step 1: Check Backend Capabilities
35
+
36
+ Always verify what the backend supports before rendering forms or performing updates.
37
+
38
+ ```typescript
39
+ const { updateEmail, updateProfile, deleteAccount } = commerce.capabilities;
40
+
41
+ if (updateEmail) {
42
+ // Render email update field
43
+ }
44
+ ```
45
+
46
+ ### Step 2: Retrieve Current Profile
47
+
48
+ Fetch the customer object to populate your forms.
49
+
50
+ ```typescript
51
+ const customer = await commerce.auth.getCurrentCustomer();
52
+ ```
53
+
54
+ ### Step 3: Implement Update Logic
55
+
56
+ Handle updates carefully, checking for specific field support.
57
+
58
+ ```typescript
59
+ async function handleUpdate(formData) {
60
+ const updateData = {};
61
+
62
+ if (commerce.capabilities.updateProfile) {
63
+ updateData.first_name = formData.firstName;
64
+ updateData.last_name = formData.lastName;
65
+ }
66
+
67
+ if (commerce.capabilities.updateEmail) {
68
+ updateData.email = formData.email;
69
+ }
70
+
71
+ return await commerce.auth.updateCustomer(updateData);
72
+ }
73
+ ```
74
+
75
+ ### Step 4: Manage Password & Security
76
+
77
+ Handle password changes based on whether the backend supports direct or email-based resets.
78
+
79
+ ```typescript
80
+ if (commerce.capabilities.changePasswordDirect) {
81
+ await commerce.auth.updateCustomer({
82
+ current_password: 'old',
83
+ password: 'new',
84
+ });
85
+ }
86
+ ```
87
+
88
+ ## ✅ DO's
89
+
90
+ 1. **Check Capabilities First:** Avoid rendering buttons for features the backend doesn't support.
91
+ 2. **Use Metadata for Extensions:** Store extra info like `avatar_url` or `notifications` in `customer.metadata`.
92
+ 3. **Handle Errors explicitly:** Distinguish between `EMAIL_TAKEN` and generic update errors.
93
+ 4. **Validate on Client:** Use patterns/regex for phone and email before sending to the adapter.
94
+ 5. **Protect UI with Loading States:** Backend operations can take time; show a spinner.
95
+
96
+ ## ❌ DONT's
97
+
98
+ 1. **Don't assume all backends are equal:** Medusa v2 has different capabilities than Shopify or WooCommerce.
99
+ 2. **Don't hardcode profile fields:** Use the `Customer` interface from `@thorprovider/adapters`.
100
+ 3. **Don't ignore account deletion:** Provide a way for users to delete their account if `deleteAccount` is true.
101
+ 4. **Don't send sensitive data in metadata:** Metadata is often visible to the user and sometimes less secure.
102
+ 5. **Don't forget to re-fetch profile:** After a successful update, the local customer state should be refreshed.
103
+
104
+ ## Common Patterns
105
+
106
+ ### Metadata-based Avatar Management
107
+ Since many backends don't support native avatars, use an external service (S3/Cloudinary) and store the URL in metadata.
108
+
109
+ ```typescript
110
+ await commerce.auth.updateCustomer({
111
+ metadata: { avatar_url: 'https://...' }
112
+ });
113
+ ```
114
+
115
+ ### Adaptive Profile Form
116
+ Render only the fields supported by the current provider.
117
+
118
+ ## Common Errors & Solutions
119
+
120
+ ### Error: `OPERATION_NOT_SUPPORTED`
121
+ **Cause:** Attempting to update a field (like email) on a backend that doesn't allow it.
122
+ **Solution:** Check `commerce.capabilities` before calling the method.
123
+
124
+ ### Error: `EMAIL_TAKEN`
125
+ **Cause:** User tried to change email to one already registered.
126
+ **Solution:** Show a "This email is already in use" validation message.
127
+
128
+ ## Common Pitfalls
129
+
130
+ > ⚠️ **Pitfall 1: Metadata Overwrites**
131
+ > Some backends overwrite the entire metadata object instead of merging. Always fetch the current ones first if you want to preserve existing fields.
132
+
133
+ > ⚠️ **Pitfall 2: Password Complexity**
134
+ > Backends have different password requirements. Let the backend validate and handle the error message.
135
+
136
+ ## Troubleshooting
137
+
138
+ | Issue | Solution |
139
+ |-------|----------|
140
+ | Profile data not saving | Check if the user session is still active |
141
+ | Metadata field missing | Ensure the key exists in the backend's allowed metadata list |
142
+ | 403 Forbidden | User might not have permission to update their own profile |
143
+
144
+ ## Next Steps
145
+
146
+ - [Capabilities System](capabilities-system.md)
147
+ - [Authentication](authentication.md)
@@ -0,0 +1,85 @@
1
+ # Providers Overview
2
+
3
+ ---
4
+ type: guide
5
+ tags: [architecture, providers, medusa, shopify, roadmap]
6
+ related:
7
+ - capabilities-system.md
8
+ - services-overview.md
9
+ - getting-started.md
10
+ updated: 2026-02-28
11
+ ---
12
+
13
+ Overview of the e-commerce platforms supported by Thor Adapters and how they are abstracted into a unified interface.
14
+
15
+ ## Overview
16
+
17
+ Thor Adapters use a "Provider" architecture to connect our application with different e-commerce backends. Each provider acts as a **full compatibility layer**, handling both:
18
+ - **Inbound (Read)**: mapping vendor-specific API responses to the standardized Thor commerce types.
19
+ - **Outbound (Write)**: transforming `@thorprovider/types` objects into the vendor-specific API payloads required for mutations (creating carts, updating addresses, placing orders, managing customers, etc.).
20
+
21
+ ## The Problem
22
+
23
+ E-commerce backends vary wildly in their API architecture (REST vs GraphQL), authentication methods (JWT, Cookies, OAuth), and feature sets.
24
+ - **Vendor Lock-in:** Migrating from one backend to another usually requires a total rewrite of the frontend data layer.
25
+ - **Maintenance Burden:** Supporting multiple backends often leads to fragmented codebases with many `if/else` checks for backend types.
26
+
27
+ Thor Adapters eliminate these problems by wrapping the data layer in a consistent interface.
28
+
29
+ ## Current Support Matrix
30
+
31
+ ### Medusa (v2.0)
32
+ **Status:** ✅ Production Ready
33
+ Our primary integration, highly optimized for headless performance.
34
+
35
+ ### Shopify
36
+ **Status:** 🚧 In Development (Phase 1)
37
+ Focusing on Storefront API integration for product discovery and cart.
38
+
39
+ ### WooCommerce
40
+ **Status:** 🗓️ Planned
41
+ Targeting the REST API for hybrid headless setups.
42
+
43
+ ## Conceptual Foundation
44
+
45
+ ### 1. The Strategy Pattern
46
+ We use the strategy pattern to swap implementation at runtime. Your application logic doesn't care if a product comes from Medusa or Shopify; it only sees the `Product` interface.
47
+
48
+ ### 2. Capabilities-Driven Development
49
+ Instead of checking `if (provider === 'medusa')`, we check `if (commerce.capabilities.canAddMetadata)`. This makes the frontend future-proof as backends evolve.
50
+
51
+ ## Architecture & Relationships
52
+
53
+ ```mermaid
54
+ graph TD
55
+ A[createProvider Factory] --> B{Configuration}
56
+ B --> C[Medusa Provider]
57
+ B --> D[Shopify Provider]
58
+ B --> E[Mock Provider]
59
+ C --> F[Unified Commerce Interface]
60
+ D --> F
61
+ E --> F
62
+ ```
63
+
64
+ ## Design Decisions
65
+
66
+ - **Medusa-First:** Since Medusa v2 is our primary backend, our standardized types are heavily influenced by its structure (e.g., variant handling).
67
+ - **Graceful Degradation:** If a provider doesn't support a feature (like "Gift Cards"), the corresponding service returns a standardized `NotSupportedError`.
68
+ - **Statelessness:** Providers avoid local state to stay predictable in server-side contexts.
69
+
70
+ ## Common Misconceptions
71
+
72
+ - **"All providers are identical"**: While the interface is identical, the *behavior* differs (e.g., login speed, pagination limits).
73
+ - **"I can easily switch in production"**: While the code supports it, data migration between backends is still a separate, complex manual task.
74
+ - **"Every provider uses GraphQL"**: We use whatever is most efficient for that backend (GraphQL for Shopify, REST for Medusa).
75
+
76
+ ## Anti-Patterns
77
+
78
+ 1. **Backend-Specific Logic in UI:** Avoid `if (commerce.name === 'medusa')` in your React components.
79
+ 2. **Hardcoding Search Syntax:** Search syntax differs across providers; use the standardized `getProducts({ query: '...' })` method.
80
+
81
+ ## Next Steps
82
+
83
+ - [Reference: Capabilities System](capabilities-system.md)
84
+ - [How-to: Getting Started](getting-started.md)
85
+ - [Workflow: Add New Provider](../../../docs/workflows/legacy/add-provider.md)
@@ -0,0 +1,135 @@
1
+ # Services Overview
2
+
3
+ ---
4
+ type: guide
5
+ tags: [architecture, services, commerce, api, architecture]
6
+ related:
7
+ - getting-started.md
8
+ - capabilities-system.md
9
+ - providers-overview.md
10
+ updated: 2026-02-28
11
+ ---
12
+
13
+ Overview of the functional services provided by Thor adapters and how they interact to provide a unified e-commerce interface.
14
+
15
+ ## Overview
16
+
17
+ Thor adapters organize commerce functionality into discrete services. This abstraction allows the rest of the application to remain backend-agnostic, interacting with a standardized set of methods regardless of whether the provider is Medusa, Shopify, or a custom backend.
18
+
19
+ ## The Problem
20
+
21
+ Directly integrating with multiple commerce backends is difficult because:
22
+ - **Inconsistent Schemas:** Every API has different nesting and field names for a "Product".
23
+ - **Varied Authentication:** Some use JWT, some use Cookies, some use Oauth.
24
+ - **Feature Disparity:** Not all backends support "Related Products" or "Gift Cards".
25
+
26
+ Thor Adapters solve this by providing a unified service layer and a capabilities system to handle these differences gracefully.
27
+
28
+ ## Core Services
29
+
30
+ ### 1. Product Service
31
+ Responsible for retrieving product data, searching, and obtaining recommendations.
32
+
33
+ **Key capabilities:**
34
+ - Multi-variant support
35
+ - Standardized price ranges
36
+ - Cross-sell/Upsell recommendations
37
+
38
+ ### 2. Collection Service
39
+ Handles grouping of products into categories or collections.
40
+
41
+ **Key capabilities:**
42
+ - Collection hierarchies
43
+ - Paginated results per collection
44
+
45
+ ### 3. Cart Service
46
+ Manages the lifecycle of a shopping cart, from creation to checkout preparation.
47
+
48
+ **Key capabilities:**
49
+ - Unified line items
50
+ - Dynamic tax and total calculations
51
+ - Simple add/update/remove operations
52
+
53
+ #### Medusa v2 note: line item totals
54
+ In Medusa v2 Store API responses, cart line item totals such as `total`, `subtotal`, and `original_total` are only returned when you retrieve cart totals using the recommended query/expansions.
55
+
56
+ Thor Adapters treat `unit_price` as the reliable baseline (always present) and will fall back to computing a line total as `unit_price × quantity` when totals fields are missing. This prevents `$0.00` line items in the UI while still allowing accurate totals when Medusa returns them.
57
+
58
+ ### 4. Authentication Service
59
+ Handles customer account management and secure sessions.
60
+
61
+ **Key capabilities:**
62
+ - JWT & Session support
63
+ - Profile and address management
64
+ - Metadata extensions
65
+
66
+ ## Service Interactions
67
+
68
+ ```mermaid
69
+ graph LR
70
+ A[App] --> B[Commerce Instance]
71
+ B --> C[Product Service]
72
+ B --> D[Cart Service]
73
+ B --> E[Auth Service]
74
+ C -.-> D
75
+ D -.-> E
76
+ ```
77
+
78
+ 1. **Discovery:** User explores products via Product Service.
79
+ 2. **Action:** User adds to cart via Cart Service (which might link to the Customer).
80
+ 3. **Checkout:** Cart Service redirects to the provider's checkout or prepares data for a custom one.
81
+
82
+ ## Design Decisions
83
+
84
+ ### Standardized Money Object
85
+ All prices are returned as a `Money` object with `amount` (string) and `currencyCode` to avoid floating-point errors.
86
+
87
+ ### Universal Cursor-based Pagination
88
+ We use cursor-based pagination whenever possible to maintain compatibility with modern backends like Shopify and Medusa v2.
89
+
90
+ ### Fail-fast with Typed Errors
91
+ Instead of returning `null` on errors, we throw a `CommerceError` with a specific `code`, allowing the UI to react appropriately.
92
+
93
+ ## Common Misconceptions
94
+
95
+ - **"All methods are always available"**: Some backends don't support specific features. Always check `commerce.can('feature')`.
96
+ - **"Updates are immediate in the backend"**: Some providers use eventual consistency. Wait for the response before updating local state.
97
+ - **"The adapter stores state"**: Outside of auth tokens, adapters are largely stateless.
98
+
99
+ ## Anti-Patterns
100
+
101
+ 1. **Hardcoding IDs:** Never hardcode product or variant IDs; always fetch them through the services.
102
+ 2. **Ignoring Capabilities:** Calling `auth.deleteAccount` without checking if it's supported will throw an error.
103
+ 3. **Direct API calls:** **NEVER** use `fetch` to talk to the backend directly from outside `@thorprovider/adapters`. If the adapter doesn't expose the method you need, add it to `@thorprovider/adapters` first, then consume it from L5. This is non-negotiable — there are no exceptions, not even for Admin API operations.
104
+
105
+ ## Next Steps
106
+
107
+ - [Reference: Capabilities System](capabilities-system.md)
108
+ - [How-to: Authentication](authentication.md)
109
+ - [How-to: Profile Management](profile-management.md)
110
+
111
+ ## Type Safety
112
+
113
+ All services are fully typed with TypeScript:
114
+
115
+ ```typescript
116
+ // TypeScript knows the return type
117
+ const products: Product[] = await commerce.getProducts();
118
+
119
+ // TypeScript validates input
120
+ await commerce.addToCart(cart.id!, [
121
+ {
122
+ merchandiseId: 'variant_123', // ✅ Required
123
+ quantity: 2, // ✅ Required
124
+ customAttribute: 'value', // ❌ Error: invalid property
125
+ },
126
+ ]);
127
+ ```
128
+
129
+ ## Resources
130
+
131
+ - [Capabilities System](./capabilities-system.md) - Feature detection
132
+ - [Providers Overview](./providers-overview.md) - Backend support
133
+ - [Authentication](./authentication.md) - Auth details
134
+ - [Profile Management](./profile-management.md) - Customer operations
135
+ - [TypeScript Types](../src/types/) - Full type definitions