medusa-contact-us 0.0.11 → 0.0.20

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 (48) hide show
  1. package/.medusa/server/src/admin/index.js +4 -494
  2. package/.medusa/server/src/admin/index.mjs +6 -496
  3. package/.medusa/server/src/api/admin/contact-email-subscriptions/route.js +3 -3
  4. package/.medusa/server/src/api/admin/contact-requests/[id]/route.js +10 -34
  5. package/.medusa/server/src/api/admin/contact-requests/[id]/status/route.js +24 -0
  6. package/.medusa/server/src/api/admin/contact-requests/route.js +47 -11
  7. package/.medusa/server/src/api/admin/contact-requests/validators.js +22 -12
  8. package/.medusa/server/src/api/store/contact-email-subscriptions/route.js +3 -3
  9. package/.medusa/server/src/api/store/contact-email-subscriptions/validators.js +2 -2
  10. package/.medusa/server/src/api/store/contact-requests/route.js +4 -10
  11. package/.medusa/server/src/api/store/contact-requests/validators.js +4 -8
  12. package/.medusa/server/src/constants.js +10 -4
  13. package/.medusa/server/src/helpers/contact-request.js +52 -0
  14. package/.medusa/server/src/helpers/contact-subscription.js +15 -4
  15. package/.medusa/server/src/helpers/index.js +5 -4
  16. package/.medusa/server/src/index.js +16 -13
  17. package/.medusa/server/src/modules/contact-requests/index.js +4 -5
  18. package/.medusa/server/src/modules/contact-requests/migrations/Migration20241129163317.js +47 -0
  19. package/.medusa/server/src/modules/contact-requests/models/contact-request.js +4 -3
  20. package/.medusa/server/src/modules/contact-requests/service.js +191 -141
  21. package/.medusa/server/src/modules/contact-requests/utils/resolve-options.js +48 -0
  22. package/.medusa/server/src/modules/contact-subscriptions/index.js +4 -5
  23. package/.medusa/server/src/modules/contact-subscriptions/service.js +20 -12
  24. package/.medusa/server/src/types/contact-request.js +3 -0
  25. package/.medusa/server/src/types.js +1 -208
  26. package/.medusa/server/src/workflows/create-contact-request-workflow.js +13 -0
  27. package/.medusa/server/src/workflows/index.js +5 -5
  28. package/.medusa/server/src/workflows/steps/create-contact-request-step.js +3 -7
  29. package/.medusa/server/src/workflows/steps/resolve-status-transition-step.js +15 -0
  30. package/.medusa/server/src/workflows/steps/send-status-notification-step.js +56 -0
  31. package/.medusa/server/src/workflows/steps/update-contact-request-status-step.js +7 -7
  32. package/.medusa/server/src/workflows/update-contact-request-status-workflow.js +33 -0
  33. package/README.md +329 -211
  34. package/package.json +2 -2
  35. package/.medusa/server/src/api/admin/contact-requests/[id]/comments/route.js +0 -22
  36. package/.medusa/server/src/api/admin/plugin/route.js +0 -11
  37. package/.medusa/server/src/api/store/plugin/route.js +0 -11
  38. package/.medusa/server/src/helpers/__tests__/submit-contact-request.test.js +0 -125
  39. package/.medusa/server/src/helpers/submit-contact-request.js +0 -45
  40. package/.medusa/server/src/modules/contact-requests/migrations/Migration20241124090000.js +0 -51
  41. package/.medusa/server/src/modules/contact-requests/models/contact-request-comment.js +0 -11
  42. package/.medusa/server/src/plugin-options.js +0 -16
  43. package/.medusa/server/src/types/__tests__/contact-options.test.js +0 -83
  44. package/.medusa/server/src/utils/__tests__/payload-validator.test.js +0 -81
  45. package/.medusa/server/src/utils/payload.js +0 -127
  46. package/.medusa/server/src/workflows/create-contact-request.js +0 -30
  47. package/.medusa/server/src/workflows/steps/send-contact-notification-step.js +0 -48
  48. package/.medusa/server/src/workflows/update-contact-request-status.js +0 -32
package/README.md CHANGED
@@ -1,17 +1,30 @@
1
1
  <h1 align="center">Medusa Contact Us Plugin</h1>
2
2
 
3
- Collect, triage, and resolve storefront “contact us” submissions inside the Medusa Admin. The plugin ships a full stack experience: configurable form schema, database-backed requests/comments, workflows with notification hooks, open store endpoint, admin APIs, helper utilities, and a polished admin UI.
3
+ A comprehensive Medusa v2 plugin for managing contact requests and email subscriptions. The plugin provides a complete solution: database-backed contact requests with configurable status workflows, email subscriptions, admin APIs, helper utilities, and a polished admin UI.
4
+
5
+ **Features:**
6
+ - 📝 Contact request management with status workflow and email notifications
7
+ - ✉️ Email subscription management (opt-ins and opt-outs)
8
+ - 🔄 Configurable status transitions and validation
9
+ - 📧 Automatic email notifications on status changes
10
+ - 🎯 Payload validation against configurable field definitions
4
11
 
5
12
  ## Features
6
13
 
7
- - Configurable form schema (required email + arbitrary fields validated from plugin options)
8
- - 🧩 Status lifecycle definition (initial/intermediate/final, allowed transitions, metadata)
9
- - ✉️ Workflow-powered notifications on submission and on final status (re-uses Medusa notification module)
10
- - 🗒️ Admin-only comment trail per request with rich status history
11
- - 🖥️ React Admin extension with list + detail views, filters, status change controls, and comment composer
12
- - 🔌 Frontend helper (`submitContactRequest`) to abstract API wiring
14
+ ### Email Subscriptions
13
15
  - ✉️ Storefront opt-in helper + admin list for email subscriptions (subscribed/unsubscribed)
14
- - 🧪 Table-driven tests for payload validation and helper logic
16
+ - 🖥️ React Admin extension with list view, filters, and search
17
+ - 🔌 Frontend helper (`upsertContactSubscription`) to abstract API wiring
18
+ - 🧪 Table-driven tests for helper logic
19
+
20
+ ### Contact Requests
21
+ - 📝 Contact request management with configurable status workflow
22
+ - 🔄 Status transition validation with configurable allowed transitions
23
+ - 📧 Email notifications on status changes (configurable per transition)
24
+ - 🎯 Payload validation against configurable field definitions
25
+ - 📊 Admin API for listing, filtering, and managing requests
26
+ - 🔌 Storefront helper (`submitContactRequest`) for easy integration
27
+ - 📈 Status history tracking with timestamps and actor information
15
28
 
16
29
  ## Installation
17
30
 
@@ -23,160 +36,85 @@ npm install medusa-contact-us
23
36
 
24
37
  ## Configuration
25
38
 
26
- Inside `medusa-config.ts` register the plugin and tailor the options:
39
+ Inside `medusa-config.ts` register the plugin:
27
40
 
28
41
  ```ts
29
42
  import type { ConfigModule } from "@medusajs/framework/types"
30
43
  import {
31
- defineContactUsPluginOptions,
32
- ContactRequestModule,
33
44
  ContactSubscriptionModule,
45
+ ContactRequestModule,
34
46
  } from "medusa-contact-us"
35
47
 
36
48
  const plugins = [
37
49
  {
38
50
  resolve: "medusa-contact-us",
39
- options: defineContactUsPluginOptions({
40
- // Form configuration
41
- form: {
42
- // Maximum payload size in KB
43
- max_payload_kb: 128,
44
- // Additional custom fields beyond the required email field
45
- additional_fields: [
46
- {
47
- key: "subject",
48
- label: "Subject",
49
- description: "Brief description of your inquiry",
50
- type: "text",
51
- required: true,
52
- placeholder: "e.g., Order inquiry",
53
- helper_text: "Please provide a clear subject line",
54
- },
55
- {
56
- key: "message",
57
- label: "Message",
58
- description: "Detailed message about your inquiry",
59
- type: "textarea",
60
- required: true,
61
- placeholder: "Please describe your issue or question...",
62
- },
63
- {
64
- key: "priority",
65
- label: "Priority",
66
- description: "How urgent is your request?",
67
- type: "select",
68
- required: false,
69
- options: [
70
- { value: "low", label: "Low" },
71
- { value: "medium", label: "Medium" },
72
- { value: "high", label: "High" },
73
- { value: "urgent", label: "Urgent" },
74
- ],
75
- },
76
- {
77
- key: "order_number",
78
- label: "Order Number",
79
- description: "If this relates to an order, please provide the order number",
80
- type: "text",
81
- required: false,
82
- placeholder: "order_123456",
83
- },
84
- {
85
- key: "phone",
86
- label: "Phone Number",
87
- type: "text",
88
- required: false,
89
- placeholder: "+1 (555) 123-4567",
90
- },
91
- {
92
- key: "preferred_contact_method",
93
- label: "Preferred Contact Method",
94
- type: "select",
95
- required: false,
96
- options: [
97
- { value: "email", label: "Email" },
98
- { value: "phone", label: "Phone" },
99
- ],
100
- },
101
- {
102
- key: "is_return_request",
103
- label: "Is this a return request?",
104
- type: "boolean",
105
- required: false,
106
- },
107
- ],
108
- },
109
- // Status lifecycle configuration
110
- statuses: {
111
- // Initial status when a request is created
112
- initial: "new",
113
- // Intermediate statuses (work in progress)
114
- intermediates: ["in_review", "waiting_for_customer"],
115
- // Final status (request is complete)
116
- final: "closed",
117
- // Allowed status transitions
118
- transitions: {
119
- new: ["in_review", "closed"],
120
- in_review: ["waiting_for_customer", "closed"],
121
- waiting_for_customer: ["in_review", "closed"],
51
+ options: {
52
+ // Contact Request Configuration
53
+ default_status: "pending",
54
+ payload_fields: [
55
+ {
56
+ key: "subject",
57
+ type: "text",
58
+ required: true,
59
+ label: "Subject",
60
+ placeholder: "Enter subject",
61
+ },
62
+ {
63
+ key: "message",
64
+ type: "textarea",
65
+ required: true,
66
+ label: "Message",
67
+ placeholder: "Enter your message",
122
68
  },
123
- },
124
- // Status options with labels and notification settings
125
- status_options: [
126
69
  {
127
- code: "new",
128
- label: "New",
129
- description: "Recently submitted requests awaiting review",
130
- notify_customer: true,
131
- template: "emails/contact-received.mjml",
70
+ key: "priority",
71
+ type: "select",
72
+ required: false,
73
+ label: "Priority",
74
+ options: [
75
+ { value: "low", label: "Low" },
76
+ { value: "medium", label: "Medium" },
77
+ { value: "high", label: "High" },
78
+ ],
79
+ },
80
+ ],
81
+ allowed_statuses: ["pending", "in_progress", "resolved", "closed"],
82
+ status_transitions: [
83
+ {
84
+ from: null,
85
+ to: "pending",
86
+ send_email: false,
132
87
  },
133
88
  {
134
- code: "in_review",
135
- label: "In Review",
136
- description: "Assigned to an agent and being reviewed",
137
- notify_customer: false,
89
+ from: "pending",
90
+ to: "in_progress",
91
+ send_email: true,
92
+ email_subject: "Your request is being processed",
93
+ email_template: null, // Optional: path to email template
138
94
  },
139
95
  {
140
- code: "waiting_for_customer",
141
- label: "Waiting for Customer",
142
- description: "Awaiting response from customer",
143
- notify_customer: true,
144
- template: "emails/contact-waiting.mjml",
96
+ from: "in_progress",
97
+ to: "resolved",
98
+ send_email: true,
99
+ email_subject: "Your request has been resolved",
145
100
  },
146
101
  {
147
- code: "closed",
148
- label: "Closed",
149
- description: "Resolved and completed",
150
- notify_customer: true,
151
- template: "emails/contact-final.mjml",
102
+ from: "resolved",
103
+ to: "closed",
104
+ send_email: false,
152
105
  },
153
106
  ],
154
- // Notification configuration
155
- notifications: {
156
- // Send email when a contact request is created
157
- send_on_create: true,
158
- // Template for acknowledgement email
159
- acknowledgement_template: "emails/contact-received.mjml",
160
- // Send email when request reaches final status
161
- send_on_final_status: true,
162
- // Optional: Custom from address
163
- from_address: "support@yourstore.com",
164
- // Optional: Reply-to address
165
- reply_to: "support@yourstore.com",
166
- },
167
- // Comments configuration
168
- comments: {
169
- // Enable admin comments on contact requests
107
+ email: {
170
108
  enabled: true,
171
- // Require a note when changing status to final
172
- require_note_on_final: true,
109
+ default_subject: "Contact Request Status Update",
110
+ default_template: null, // Optional: default email template path
173
111
  },
174
- }),
112
+ },
175
113
  },
176
114
  ]
177
115
 
178
- // Register the ContactRequestModule
179
- const modules = [ContactRequestModule, ContactSubscriptionModule]
116
+ // Register the modules
117
+ const modules = [ContactSubscriptionModule, ContactRequestModule]
180
118
 
181
119
  export default {
182
120
  projectConfig: {
@@ -189,50 +127,87 @@ export default {
189
127
  } satisfies ConfigModule
190
128
  ```
191
129
 
192
- ### Field Types
193
-
194
- Available field types for `additional_fields`:
195
- - `text`: Single-line text input
196
- - `textarea`: Multi-line text input
197
- - `number`: Numeric input
198
- - `select`: Dropdown selection (requires `options` array)
199
- - `multi_select`: Multiple selection (requires `options` array)
200
- - `boolean`: Checkbox
201
- - `date`: Date picker
202
-
203
- ### Status lifecycle best practices
204
-
205
- - Define unique status codes (kebab or snake case) and map them in `status_options`.
206
- - Provide at least one intermediate state when multiple review steps exist.
207
- - Set `notify_customer` to `true` for statuses that should trigger emails.
208
- - Configure `transitions` if you need granular control; otherwise the default is `(all non-final statuses) -> intermediates/final`.
130
+ ### Configuration Options
131
+
132
+ #### Contact Request Options
133
+
134
+ - **`default_status`** (string, optional): Default status for new requests. Default: `"pending"`
135
+ - **`payload_fields`** (array, optional): Field definitions for payload validation. Each field supports:
136
+ - `key` (string, required): Field identifier
137
+ - `type` (string, required): Field type - `"text"`, `"textarea"`, `"number"`, `"email"`, `"select"`, `"checkbox"`
138
+ - `required` (boolean, optional): Whether field is required
139
+ - `label` (string, optional): Display label
140
+ - `placeholder` (string, optional): Placeholder text
141
+ - `options` (array, optional): For select type - `[{ value: string, label: string }]`
142
+ - `validation` (object, optional): Validation rules (min, max, pattern)
143
+ - **`allowed_statuses`** (array, optional): List of allowed status values. Default: `["pending", "in_progress", "resolved", "closed"]`
144
+ - **`status_transitions`** (array, optional): Allowed status transitions. Each transition supports:
145
+ - `from` (string | null): Source status (null = initial status)
146
+ - `to` (string, required): Target status
147
+ - `send_email` (boolean, optional): Whether to send email on this transition
148
+ - `email_subject` (string, optional): Custom email subject for this transition
149
+ - `email_template` (string | null, optional): Custom email template path
150
+ - **`email`** (object, optional): Email notification settings
151
+ - `enabled` (boolean, optional): Enable/disable email notifications. Default: `true`
152
+ - `default_subject` (string, optional): Default email subject. Default: `"Contact Request Status Update"`
153
+ - `default_template` (string | null, optional): Default email template path
209
154
 
210
155
  ## REST API
211
156
 
212
157
  ### Storefront (open)
213
158
 
159
+ #### Contact Requests
160
+
161
+ Create a contact request:
162
+
214
163
  ```bash
215
164
  curl -X POST https://your-medusa.com/store/contact-requests \
216
165
  -H "Content-Type: application/json" \
166
+ -H "x-publishable-api-key: pk_storefront" \
217
167
  -d '{
218
168
  "email": "customer@example.com",
219
- "payload": { "subject": "Need help", "priority": "high" },
220
- "metadata": { "order_id": "order_123" }
169
+ "payload": {
170
+ "subject": "Order inquiry",
171
+ "message": "I need help with my order #12345"
172
+ },
173
+ "metadata": {
174
+ "source_page": "contact"
175
+ },
176
+ "source": "storefront"
221
177
  }'
222
178
  ```
223
179
 
180
+ Body fields:
181
+ - `email` – required, valid email address
182
+ - `payload` – optional, JSON object with custom fields (validated against configured `payload_fields`)
183
+ - `metadata` – optional, additional metadata
184
+ - `source` – optional, request source identifier (default: `"storefront"`)
185
+
224
186
  Response:
225
187
 
226
188
  ```json
227
189
  {
228
- "contact_request": {
229
- "id": "crq_123",
190
+ "request": {
191
+ "id": "creq_123",
230
192
  "email": "customer@example.com",
231
- "status": "new",
232
- "payload": { "subject": "Need help", "priority": "high" },
193
+ "payload": {
194
+ "subject": "Order inquiry",
195
+ "message": "I need help with my order #12345"
196
+ },
197
+ "status": "pending",
233
198
  "status_history": [
234
- { "status": "new", "updated_at": "2024-11-24T10:00:00.000Z" }
235
- ]
199
+ {
200
+ "from": null,
201
+ "to": "pending",
202
+ "changed_at": "2024-11-29T16:33:17.000Z"
203
+ }
204
+ ],
205
+ "metadata": {
206
+ "source_page": "contact"
207
+ },
208
+ "source": "storefront",
209
+ "created_at": "2024-11-29T16:33:17.000Z",
210
+ "updated_at": "2024-11-29T16:33:17.000Z"
236
211
  }
237
212
  }
238
213
  ```
@@ -271,75 +246,142 @@ Response:
271
246
 
272
247
  ### Admin (requires admin auth cookie/token)
273
248
 
274
- List:
249
+ #### Contact Requests
250
+
251
+ List contact requests:
275
252
 
276
253
  ```bash
277
- curl -X GET "https://your-medusa.com/admin/contact-requests?status=new&limit=20" \
254
+ curl -X GET "https://your-medusa.com/admin/contact-requests?status=pending&email=customer@example.com&limit=20&offset=0" \
278
255
  -H "Authorization: Bearer <token>"
279
256
  ```
280
257
 
281
- Detail:
258
+ Query parameters:
259
+ - `email` – optional, filter by email (partial match)
260
+ - `status` – optional, filter by status
261
+ - `source` – optional, filter by source
262
+ - `created_at.gte` – optional, filter by creation date (ISO 8601)
263
+ - `created_at.lte` – optional, filter by creation date (ISO 8601)
264
+ - `limit` – optional, number of results (default: 20, max: 100)
265
+ - `offset` – optional, pagination offset (default: 0)
266
+ - `order` – optional, sort field: `created_at`, `updated_at`, `email` (default: `created_at`)
267
+ - `order_direction` – optional, sort direction: `ASC` or `DESC` (default: `DESC`)
268
+
269
+ Get contact request details:
282
270
 
283
271
  ```bash
284
- curl -X GET https://your-medusa.com/admin/contact-requests/crq_123 \
272
+ curl -X GET "https://your-medusa.com/admin/contact-requests/creq_123" \
285
273
  -H "Authorization: Bearer <token>"
286
274
  ```
287
275
 
288
- Update status:
276
+ Response includes the request and `next_allowed_statuses` array for the admin UI:
277
+
278
+ ```json
279
+ {
280
+ "request": {
281
+ "id": "creq_123",
282
+ "email": "customer@example.com",
283
+ "payload": { ... },
284
+ "status": "pending",
285
+ "status_history": [ ... ],
286
+ "metadata": { ... },
287
+ "source": "storefront",
288
+ "created_at": "2024-11-29T16:33:17.000Z",
289
+ "updated_at": "2024-11-29T16:33:17.000Z"
290
+ },
291
+ "next_allowed_statuses": ["in_progress"]
292
+ }
293
+ ```
294
+
295
+ Create contact request (admin):
289
296
 
290
297
  ```bash
291
- curl -X PATCH https://your-medusa.com/admin/contact-requests/crq_123 \
292
- -H "Authorization: Bearer <token>" \
298
+ curl -X POST https://your-medusa.com/admin/contact-requests \
293
299
  -H "Content-Type: application/json" \
294
- -d '{ "status": "closed", "note": "Resolved via refund" }'
300
+ -H "Authorization: Bearer <token>" \
301
+ -d '{
302
+ "email": "customer@example.com",
303
+ "payload": {
304
+ "subject": "Support request",
305
+ "message": "Need assistance"
306
+ },
307
+ "source": "admin"
308
+ }'
295
309
  ```
296
310
 
297
- Add comment:
311
+ Update contact request status:
298
312
 
299
313
  ```bash
300
- curl -X POST https://your-medusa.com/admin/contact-requests/crq_123/comments \
301
- -H "Authorization: Bearer <token>" \
314
+ curl -X POST https://your-medusa.com/admin/contact-requests/creq_123/status \
302
315
  -H "Content-Type: application/json" \
303
- -d '{ "comment": "Waiting on supplier" }'
316
+ -H "Authorization: Bearer <token>" \
317
+ -d '{
318
+ "status": "in_progress"
319
+ }'
304
320
  ```
305
321
 
306
- ## Admin UI
322
+ **Note**: Only status transitions defined in `status_transitions` configuration are allowed. The API will return an error if an invalid transition is attempted.
307
323
 
308
- After running `medusa admin dev --plugins medusa-contact-us`, the sidebar will include **Contact Requests**:
324
+ #### Email Subscriptions
309
325
 
310
- - **List view** – search by email, filter by status, inspect creation dates, open details with a single click.
311
- - **Detail view** – see submitted fields, live status badge, change status (with optional notes), inspect the full history timeline, and append internal comments.
312
- - **Comments** – only admins can add comments. Comments are persisted and shown newest first.
313
- - **Contact email list** – dedicated table that displays every storefront opt-in, highlights unsubscribes, and supports filtering/searching by status or email.
326
+ List subscriptions:
314
327
 
315
- All UI components follow the Medusa UI kit spacing (8pt grid), color, and accessibility guidelines.
328
+ ```bash
329
+ curl -X GET "https://your-medusa.com/admin/contact-email-subscriptions?status=subscribed&limit=20" \
330
+ -H "Authorization: Bearer <token>"
331
+ ```
332
+
333
+ Query parameters:
334
+ - `status` – optional, filter by `subscribed` or `unsubscribed`
335
+ - `q` – optional, search by email
336
+ - `limit` – optional, number of results (default: 20)
337
+ - `offset` – optional, pagination offset
338
+
339
+ ## Admin UI
340
+
341
+ After running `medusa admin dev --plugins medusa-contact-us`, the sidebar will include **Contact email list**:
342
+
343
+ - **List view** – search by email, filter by status (subscribed/unsubscribed), inspect creation dates and unsubscribe timestamps.
344
+ - All UI components follow the Medusa UI kit spacing (8pt grid), color, and accessibility guidelines.
316
345
 
317
346
  ## Frontend helper
318
347
 
319
348
  Skip hand-writing `fetch` calls by importing the provided helpers. **Storefront requests must include a publishable API key** (create one under `Settings → API Keys` in the Medusa admin). The helpers automatically attach the header for you.
320
349
 
321
- ### Contact requests
350
+ ### Contact Requests
351
+
352
+ Submit a contact request from your storefront:
322
353
 
323
354
  ```ts
324
- import { submitContactRequest } from "medusa-contact-us"
355
+ import { submitContactRequest } from "medusa-contact-us/helpers"
325
356
 
326
- await submitContactRequest(
357
+ const result = await submitContactRequest(
327
358
  {
328
359
  email: "customer@example.com",
329
- payload: { subject: "Question", priority: "high" },
360
+ payload: {
361
+ subject: "Order inquiry",
362
+ message: "I need help with my order #12345",
363
+ priority: "high",
364
+ },
365
+ metadata: {
366
+ source_page: "contact",
367
+ user_agent: navigator.userAgent,
368
+ },
369
+ source: "storefront",
330
370
  },
331
371
  {
332
372
  baseUrl: "https://store.myshop.com",
333
373
  publishableApiKey: "pk_test_storefront",
334
374
  }
335
375
  )
376
+
377
+ console.log("Request created:", result.request.id)
336
378
  ```
337
379
 
338
- Using a Medusa JS client keeps credentials in one place while still letting you override headers (including publishable keys) per call:
380
+ Using a Medusa JS client:
339
381
 
340
382
  ```ts
341
383
  import Medusa from "@medusajs/medusa-js"
342
- import { submitContactRequest } from "medusa-contact-us"
384
+ import { submitContactRequest } from "medusa-contact-us/helpers"
343
385
 
344
386
  const medusa = new Medusa({
345
387
  baseUrl: "https://store.myshop.com",
@@ -349,35 +391,36 @@ const medusa = new Medusa({
349
391
  await submitContactRequest(
350
392
  {
351
393
  email: "customer@example.com",
352
- payload: { subject: "Returns" },
394
+ payload: {
395
+ subject: "Support request",
396
+ message: "Need assistance",
397
+ },
353
398
  },
354
399
  {
355
400
  client: medusa,
356
401
  publishableApiKey: "pk_live_client",
357
- headers: {
358
- Cookie: "connect.sid=...",
359
- },
360
402
  }
361
403
  )
362
404
  ```
363
405
 
364
- For SSR or edge runtimes, preconfigure the helper once:
406
+ For SSR or edge runtimes, preconfigure the helper:
365
407
 
366
408
  ```ts
367
- import { createSubmitContactRequest } from "medusa-contact-us"
409
+ import { createSubmitContactRequest } from "medusa-contact-us/helpers"
368
410
 
369
- const submitContactRequest = createSubmitContactRequest({
411
+ export const submitRequest = createSubmitContactRequest({
370
412
  baseUrl: process.env.NEXT_PUBLIC_MEDUSA_URL,
371
413
  publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
372
414
  })
373
415
 
374
416
  export async function action(formData: FormData) {
375
- await submitContactRequest({
417
+ await submitRequest({
376
418
  email: formData.get("email") as string,
377
419
  payload: {
378
- subject: formData.get("subject"),
379
- message: formData.get("message"),
420
+ subject: formData.get("subject") as string,
421
+ message: formData.get("message") as string,
380
422
  },
423
+ source: "contact_form",
381
424
  })
382
425
  }
383
426
  ```
@@ -400,7 +443,33 @@ await upsertContactSubscription(
400
443
  )
401
444
  ```
402
445
 
403
- To keep deploys DRY, build a preconfigured helper and reuse it everywhere:
446
+ Using a Medusa JS client keeps credentials in one place while still letting you override headers (including publishable keys) per call:
447
+
448
+ ```ts
449
+ import Medusa from "@medusajs/medusa-js"
450
+ import { upsertContactSubscription } from "medusa-contact-us"
451
+
452
+ const medusa = new Medusa({
453
+ baseUrl: "https://store.myshop.com",
454
+ publishableKey: "pk_live_client",
455
+ })
456
+
457
+ await upsertContactSubscription(
458
+ {
459
+ email: "newsletter@example.com",
460
+ status: "subscribed",
461
+ },
462
+ {
463
+ client: medusa,
464
+ publishableApiKey: "pk_live_client",
465
+ headers: {
466
+ Cookie: "connect.sid=...",
467
+ },
468
+ }
469
+ )
470
+ ```
471
+
472
+ For SSR or edge runtimes, preconfigure the helper once:
404
473
 
405
474
  ```ts
406
475
  import { createUpsertContactSubscription } from "medusa-contact-us"
@@ -410,11 +479,13 @@ export const upsertSubscription = createUpsertContactSubscription({
410
479
  publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
411
480
  })
412
481
 
413
- await upsertSubscription({
414
- email: input.email,
415
- status: input.unsubscribe ? "unsubscribed" : "subscribed",
416
- source: input.source ?? "footer_form",
417
- })
482
+ export async function action(formData: FormData) {
483
+ await upsertSubscription({
484
+ email: formData.get("email") as string,
485
+ status: input.unsubscribe ? "unsubscribed" : "subscribed",
486
+ source: input.source ?? "footer_form",
487
+ })
488
+ }
418
489
  ```
419
490
 
420
491
  ### Shared helper options
@@ -425,13 +496,58 @@ await upsertSubscription({
425
496
  - `fetchImpl` – Custom fetch implementation (SSR, React Native, etc.).
426
497
  - `headers` – Additional headers merged into the request (e.g., session cookie, localization). Values you pass here override the defaults, including the publishable key header if you need a per-request key.
427
498
 
428
- Customer-authenticated endpoints (like submitting a request from a logged-in area) still require the appropriate session cookie or JWT. Provide those via the `headers` option if theyre not already managed by the browser fetch call.
499
+ Customer-authenticated endpoints still require the appropriate session cookie or JWT. Provide those via the `headers` option if they're not already managed by the browser fetch call.
500
+
501
+ ## Status Workflow
502
+
503
+ Contact requests follow a configurable status workflow:
504
+
505
+ 1. **Initial Status**: New requests are created with the `default_status` (typically `"pending"`)
506
+ 2. **Status Transitions**: Only transitions defined in `status_transitions` are allowed
507
+ 3. **Status History**: All status changes are tracked with timestamps and actor information
508
+ 4. **Email Notifications**: Emails can be sent on specific transitions when `send_email: true`
509
+
510
+ ### Example Workflow
511
+
512
+ ```
513
+ pending → in_progress → resolved → closed
514
+ ```
515
+
516
+ In this example:
517
+ - Admin can move requests from `pending` to `in_progress` (email sent)
518
+ - Admin can move requests from `in_progress` to `resolved` (email sent)
519
+ - Admin can move requests from `resolved` to `closed` (no email)
520
+
521
+ ### Admin UI Integration
429
522
 
430
- ## Workflows & notifications
523
+ When fetching a contact request via the admin API, the response includes `next_allowed_statuses`:
431
524
 
432
- - `createContactRequestWorkflow` validates payloads, persists the request, and optionally fires an acknowledgement email.
433
- - `updateContactRequestStatusWorkflow` enforces transitions, records history entries, and emits final-status notifications.
434
- - Notifications rely on the standard Medusa notification module. Ensure at least one email provider is configured; otherwise the workflows raise an error to surface misconfiguration early.
525
+ ```json
526
+ {
527
+ "request": { ... },
528
+ "next_allowed_statuses": ["in_progress"]
529
+ }
530
+ ```
531
+
532
+ Use this array to populate a status dropdown in your admin UI, ensuring only valid transitions are shown.
533
+
534
+ ## Database Migrations
535
+
536
+ After installing the plugin, run migrations to create the required tables:
537
+
538
+ ```bash
539
+ npx medusa db:migrate
540
+ ```
541
+
542
+ This will create:
543
+ - `contact_email_subscription` table (for email subscriptions)
544
+ - `contact_request` table (for contact requests)
545
+
546
+ **Note**: If you're developing the plugin locally, generate migrations after model changes:
547
+
548
+ ```bash
549
+ npx medusa plugin:db:generate
550
+ ```
435
551
 
436
552
  ## Testing & build
437
553
 
@@ -444,6 +560,8 @@ Always run `yarn build` after development to ensure the bundler succeeds before
444
560
 
445
561
  ## Troubleshooting
446
562
 
447
- - Submit endpoint returning `422`: ensure every field defined in `form.additional_fields` is provided and matches its type.
448
- - Notifications not sent: confirm `notifications.send_on_*` flags and that the notification module is registered (the workflows throw an actionable error otherwise).
449
- - Admin UI blank: rebuild the plugin (`yarn build`) and restart the Admin app with the plugin registered in `medusa-config.ts`.
563
+ - **Admin UI blank**: Rebuild the plugin (`yarn build`) and restart the Admin app with the plugin registered in `medusa-config.ts`.
564
+ - **Status transition errors**: Ensure the transition is defined in `status_transitions` configuration. Only transitions from the current status to an allowed next status are permitted.
565
+ - **Payload validation errors**: Check that all required fields defined in `payload_fields` are provided and match the expected types.
566
+ - **Email notifications not sending**: Verify that `email.enabled` is `true` in configuration and that the transition has `send_email: true`. Ensure the notification service is properly configured in your Medusa instance.
567
+ - **Service resolution errors**: Make sure both `ContactSubscriptionModule` and `ContactRequestModule` are registered in the `modules` array in `medusa-config.ts`.