medusa-contact-us 0.0.17 → 0.0.21

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 (26) hide show
  1. package/.medusa/server/src/admin/index.js +493 -4
  2. package/.medusa/server/src/admin/index.mjs +495 -6
  3. package/.medusa/server/src/api/admin/contact-requests/[id]/route.js +17 -0
  4. package/.medusa/server/src/api/admin/contact-requests/[id]/status/route.js +24 -0
  5. package/.medusa/server/src/api/admin/contact-requests/route.js +67 -0
  6. package/.medusa/server/src/api/admin/contact-requests/validators.js +29 -0
  7. package/.medusa/server/src/api/store/contact-requests/route.js +23 -0
  8. package/.medusa/server/src/api/store/contact-requests/validators.js +11 -0
  9. package/.medusa/server/src/helpers/contact-request.js +52 -0
  10. package/.medusa/server/src/helpers/index.js +5 -2
  11. package/.medusa/server/src/index.js +10 -2
  12. package/.medusa/server/src/modules/contact-requests/index.js +14 -0
  13. package/.medusa/server/src/modules/contact-requests/migrations/Migration20241129163317.js +47 -0
  14. package/.medusa/server/src/modules/contact-requests/models/contact-request.js +15 -0
  15. package/.medusa/server/src/modules/contact-requests/service.js +236 -0
  16. package/.medusa/server/src/modules/contact-requests/utils/resolve-options.js +48 -0
  17. package/.medusa/server/src/types/contact-request.js +3 -0
  18. package/.medusa/server/src/workflows/create-contact-request-workflow.js +13 -0
  19. package/.medusa/server/src/workflows/index.js +8 -0
  20. package/.medusa/server/src/workflows/steps/create-contact-request-step.js +13 -0
  21. package/.medusa/server/src/workflows/steps/resolve-status-transition-step.js +15 -0
  22. package/.medusa/server/src/workflows/steps/send-status-notification-step.js +56 -0
  23. package/.medusa/server/src/workflows/steps/update-contact-request-status-step.js +17 -0
  24. package/.medusa/server/src/workflows/update-contact-request-status-workflow.js +33 -0
  25. package/README.md +381 -10
  26. package/package.json +2 -1
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendStatusNotificationStep = void 0;
4
+ const workflows_sdk_1 = require("@medusajs/framework/workflows-sdk");
5
+ const utils_1 = require("@medusajs/framework/utils");
6
+ /**
7
+ * Prepare email payload for notification service
8
+ */
9
+ function prepareEmailPayload(to, subject, template, contactRequest) {
10
+ const payload = {
11
+ to,
12
+ channel: "email",
13
+ data: {
14
+ subject,
15
+ contactRequest,
16
+ },
17
+ };
18
+ if (template) {
19
+ payload.template = template;
20
+ }
21
+ return payload;
22
+ }
23
+ /**
24
+ * Send email notification for status change
25
+ */
26
+ exports.sendStatusNotificationStep = (0, workflows_sdk_1.createStep)("send-status-notification", async (input, { container }) => {
27
+ // If no email subject provided, skip sending
28
+ if (!input.emailSubject) {
29
+ return new workflows_sdk_1.StepResponse({
30
+ sent: false,
31
+ });
32
+ }
33
+ const payload = prepareEmailPayload(input.contactRequest.email, input.emailSubject, input.emailTemplate ?? null, input.contactRequest);
34
+ const notificationService = container.resolve(utils_1.Modules.NOTIFICATION);
35
+ if (!notificationService) {
36
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Notification service is not configured.");
37
+ }
38
+ try {
39
+ if (typeof notificationService.create === "function") {
40
+ await notificationService.create(payload);
41
+ }
42
+ else if (typeof notificationService.createNotifications === "function") {
43
+ await notificationService.createNotifications([payload]);
44
+ }
45
+ else {
46
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, "Notification service does not support sending notifications.");
47
+ }
48
+ }
49
+ catch (error) {
50
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, `Failed to send notification: ${error instanceof Error ? error.message : String(error)}`);
51
+ }
52
+ return new workflows_sdk_1.StepResponse({
53
+ sent: true,
54
+ });
55
+ });
56
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VuZC1zdGF0dXMtbm90aWZpY2F0aW9uLXN0ZXAuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9zcmMvd29ya2Zsb3dzL3N0ZXBzL3NlbmQtc3RhdHVzLW5vdGlmaWNhdGlvbi1zdGVwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLHFFQUE0RTtBQUM1RSxxREFBZ0U7QUFlaEU7O0dBRUc7QUFDSCxTQUFTLG1CQUFtQixDQUMxQixFQUFVLEVBQ1YsT0FBZSxFQUNmLFFBQXVCLEVBQ3ZCLGNBQWlDO0lBRWpDLE1BQU0sT0FBTyxHQVFUO1FBQ0YsRUFBRTtRQUNGLE9BQU8sRUFBRSxPQUFPO1FBQ2hCLElBQUksRUFBRTtZQUNKLE9BQU87WUFDUCxjQUFjO1NBQ2Y7S0FDRixDQUFBO0lBRUQsSUFBSSxRQUFRLEVBQUUsQ0FBQztRQUNiLE9BQU8sQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFBO0lBQzdCLENBQUM7SUFFRCxPQUFPLE9BQU8sQ0FBQTtBQUNoQixDQUFDO0FBRUQ7O0dBRUc7QUFDVSxRQUFBLDBCQUEwQixHQUFHLElBQUEsMEJBQVUsRUFDbEQsMEJBQTBCLEVBQzFCLEtBQUssRUFBRSxLQUFrQyxFQUFFLEVBQUUsU0FBUyxFQUFFLEVBQUUsRUFBRTtJQUMxRCw2Q0FBNkM7SUFDN0MsSUFBSSxDQUFDLEtBQUssQ0FBQyxZQUFZLEVBQUUsQ0FBQztRQUN4QixPQUFPLElBQUksNEJBQVksQ0FBK0I7WUFDcEQsSUFBSSxFQUFFLEtBQUs7U0FDWixDQUFDLENBQUE7SUFDSixDQUFDO0lBRUQsTUFBTSxPQUFPLEdBQUcsbUJBQW1CLENBQ2pDLEtBQUssQ0FBQyxjQUFjLENBQUMsS0FBSyxFQUMxQixLQUFLLENBQUMsWUFBWSxFQUNsQixLQUFLLENBQUMsYUFBYSxJQUFJLElBQUksRUFDM0IsS0FBSyxDQUFDLGNBQWMsQ0FDckIsQ0FBQTtJQUVELE1BQU0sbUJBQW1CLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FBQyxlQUFPLENBQUMsWUFBWSxDQUdqRSxDQUFBO0lBRUQsSUFBSSxDQUFDLG1CQUFtQixFQUFFLENBQUM7UUFDekIsTUFBTSxJQUFJLG1CQUFXLENBQ25CLG1CQUFXLENBQUMsS0FBSyxDQUFDLFlBQVksRUFDOUIseUNBQXlDLENBQzFDLENBQUE7SUFDSCxDQUFDO0lBRUQsSUFBSSxDQUFDO1FBQ0gsSUFBSSxPQUFPLG1CQUFtQixDQUFDLE1BQU0sS0FBSyxVQUFVLEVBQUUsQ0FBQztZQUNyRCxNQUFNLG1CQUFtQixDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQTtRQUMzQyxDQUFDO2FBQU0sSUFBSSxPQUFPLG1CQUFtQixDQUFDLG1CQUFtQixLQUFLLFVBQVUsRUFBRSxDQUFDO1lBQ3pFLE1BQU0sbUJBQW1CLENBQUMsbUJBQW1CLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFBO1FBQzFELENBQUM7YUFBTSxDQUFDO1lBQ04sTUFBTSxJQUFJLG1CQUFXLENBQ25CLG1CQUFXLENBQUMsS0FBSyxDQUFDLGdCQUFnQixFQUNsQyw4REFBOEQsQ0FDL0QsQ0FBQTtRQUNILENBQUM7SUFDSCxDQUFDO0lBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztRQUNmLE1BQU0sSUFBSSxtQkFBVyxDQUNuQixtQkFBVyxDQUFDLEtBQUssQ0FBQyxnQkFBZ0IsRUFDbEMsZ0NBQWdDLEtBQUssWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUN6RixDQUFBO0lBQ0gsQ0FBQztJQUVELE9BQU8sSUFBSSw0QkFBWSxDQUErQjtRQUNwRCxJQUFJLEVBQUUsSUFBSTtLQUNYLENBQUMsQ0FBQTtBQUNKLENBQUMsQ0FDRixDQUFBIn0=
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.updateContactRequestStatusStep = void 0;
4
+ const workflows_sdk_1 = require("@medusajs/framework/workflows-sdk");
5
+ const contact_requests_1 = require("../../modules/contact-requests");
6
+ exports.updateContactRequestStatusStep = (0, workflows_sdk_1.createStep)("update-contact-request-status", async (input, { container }) => {
7
+ const service = container.resolve(contact_requests_1.CONTACT_REQUEST_MODULE);
8
+ // Get current request to capture previous status
9
+ const currentRequest = await service.getRequest(input.id);
10
+ const previousStatus = currentRequest.status;
11
+ const request = await service.updateStatus(input);
12
+ return new workflows_sdk_1.StepResponse({
13
+ request,
14
+ previousStatus,
15
+ });
16
+ });
17
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXBkYXRlLWNvbnRhY3QtcmVxdWVzdC1zdGF0dXMtc3RlcC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy93b3JrZmxvd3Mvc3RlcHMvdXBkYXRlLWNvbnRhY3QtcmVxdWVzdC1zdGF0dXMtc3RlcC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSxxRUFBNEU7QUFDNUUscUVBQXVFO0FBWTFELFFBQUEsOEJBQThCLEdBQUcsSUFBQSwwQkFBVSxFQUN0RCwrQkFBK0IsRUFDL0IsS0FBSyxFQUNILEtBQXNDLEVBQ3RDLEVBQUUsU0FBUyxFQUFFLEVBQ2dELEVBQUU7SUFDL0QsTUFBTSxPQUFPLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FDL0IseUNBQXNCLENBQ3ZCLENBQUE7SUFFRCxpREFBaUQ7SUFDakQsTUFBTSxjQUFjLEdBQUcsTUFBTSxPQUFPLENBQUMsVUFBVSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUMsQ0FBQTtJQUN6RCxNQUFNLGNBQWMsR0FBRyxjQUFjLENBQUMsTUFBTSxDQUFBO0lBRTVDLE1BQU0sT0FBTyxHQUFHLE1BQU0sT0FBTyxDQUFDLFlBQVksQ0FBQyxLQUFLLENBQUMsQ0FBQTtJQUVqRCxPQUFPLElBQUksNEJBQVksQ0FBdUM7UUFDNUQsT0FBTztRQUNQLGNBQWM7S0FDZixDQUFDLENBQUE7QUFDSixDQUFDLENBQ0YsQ0FBQSJ9
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.updateContactRequestStatusWorkflow = void 0;
4
+ const workflows_sdk_1 = require("@medusajs/framework/workflows-sdk");
5
+ const update_contact_request_status_step_1 = require("./steps/update-contact-request-status-step");
6
+ const resolve_status_transition_step_1 = require("./steps/resolve-status-transition-step");
7
+ const send_status_notification_step_1 = require("./steps/send-status-notification-step");
8
+ exports.updateContactRequestStatusWorkflow = (0, workflows_sdk_1.createWorkflow)("update-contact-request-status", (input) => {
9
+ // Step 1: Update status (validation happens inside the step)
10
+ const { request, previousStatus } = (0, update_contact_request_status_step_1.updateContactRequestStatusStep)(input);
11
+ // Step 2: Resolve transition config for email notification
12
+ const { transition, options } = (0, resolve_status_transition_step_1.resolveStatusTransitionStep)({
13
+ fromStatus: previousStatus,
14
+ toStatus: input.status,
15
+ });
16
+ // Step 3: Send notification if configured
17
+ (0, send_status_notification_step_1.sendStatusNotificationStep)({
18
+ contactRequest: request,
19
+ fromStatus: previousStatus,
20
+ toStatus: input.status,
21
+ emailSubject: transition?.send_email && options.email.enabled
22
+ ? transition.email_subject ?? options.email.default_subject
23
+ : undefined,
24
+ emailTemplate: transition?.send_email && options.email.enabled
25
+ ? transition.email_template ?? options.email.default_template
26
+ : undefined,
27
+ });
28
+ return new workflows_sdk_1.WorkflowResponse({
29
+ request,
30
+ });
31
+ });
32
+ exports.default = exports.updateContactRequestStatusWorkflow;
33
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXBkYXRlLWNvbnRhY3QtcmVxdWVzdC1zdGF0dXMtd29ya2Zsb3cuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvd29ya2Zsb3dzL3VwZGF0ZS1jb250YWN0LXJlcXVlc3Qtc3RhdHVzLXdvcmtmbG93LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLHFFQUcwQztBQUMxQyxtR0FBMkY7QUFDM0YsMkZBQW9GO0FBQ3BGLHlGQUFrRjtBQWFyRSxRQUFBLGtDQUFrQyxHQUFHLElBQUEsOEJBQWMsRUFDOUQsK0JBQStCLEVBQy9CLENBQ0UsS0FBOEMsRUFDYyxFQUFFO0lBQzlELDZEQUE2RDtJQUM3RCxNQUFNLEVBQUUsT0FBTyxFQUFFLGNBQWMsRUFBRSxHQUFHLElBQUEsbUVBQThCLEVBQUMsS0FBSyxDQUFDLENBQUE7SUFFekUsMkRBQTJEO0lBQzNELE1BQU0sRUFBRSxVQUFVLEVBQUUsT0FBTyxFQUFFLEdBQUcsSUFBQSw0REFBMkIsRUFBQztRQUMxRCxVQUFVLEVBQUUsY0FBYztRQUMxQixRQUFRLEVBQUUsS0FBSyxDQUFDLE1BQU07S0FDdkIsQ0FBQyxDQUFBO0lBRUYsMENBQTBDO0lBQzFDLElBQUEsMERBQTBCLEVBQUM7UUFDekIsY0FBYyxFQUFFLE9BQU87UUFDdkIsVUFBVSxFQUFFLGNBQWM7UUFDMUIsUUFBUSxFQUFFLEtBQUssQ0FBQyxNQUFNO1FBQ3RCLFlBQVksRUFDVixVQUFVLEVBQUUsVUFBVSxJQUFJLE9BQU8sQ0FBQyxLQUFLLENBQUMsT0FBTztZQUM3QyxDQUFDLENBQUMsVUFBVSxDQUFDLGFBQWEsSUFBSSxPQUFPLENBQUMsS0FBSyxDQUFDLGVBQWU7WUFDM0QsQ0FBQyxDQUFDLFNBQVM7UUFDZixhQUFhLEVBQ1gsVUFBVSxFQUFFLFVBQVUsSUFBSSxPQUFPLENBQUMsS0FBSyxDQUFDLE9BQU87WUFDN0MsQ0FBQyxDQUFDLFVBQVUsQ0FBQyxjQUFjLElBQUksT0FBTyxDQUFDLEtBQUssQ0FBQyxnQkFBZ0I7WUFDN0QsQ0FBQyxDQUFDLFNBQVM7S0FDaEIsQ0FBQyxDQUFBO0lBRUYsT0FBTyxJQUFJLGdDQUFnQixDQUFDO1FBQzFCLE9BQU87S0FDUixDQUFDLENBQUE7QUFDSixDQUFDLENBQ0YsQ0FBQTtBQUVELGtCQUFlLDBDQUFrQyxDQUFBIn0=
package/README.md CHANGED
@@ -1,14 +1,31 @@
1
- <h1 align="center">Medusa Contact Email Subscriptions Plugin</h1>
1
+ <h1 align="center">Medusa Contact Us Plugin</h1>
2
2
 
3
- Manage storefront email subscriptions (opt-ins and opt-outs) inside the Medusa Admin. The plugin provides a complete solution: database-backed subscriptions, 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
 
14
+ ### Email Subscriptions
7
15
  - ✉️ Storefront opt-in helper + admin list for email subscriptions (subscribed/unsubscribed)
8
16
  - 🖥️ React Admin extension with list view, filters, and search
9
17
  - 🔌 Frontend helper (`upsertContactSubscription`) to abstract API wiring
10
18
  - 🧪 Table-driven tests for helper logic
11
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
28
+
12
29
  ## Installation
13
30
 
14
31
  ```bash
@@ -25,16 +42,79 @@ Inside `medusa-config.ts` register the plugin:
25
42
  import type { ConfigModule } from "@medusajs/framework/types"
26
43
  import {
27
44
  ContactSubscriptionModule,
45
+ ContactRequestModule,
28
46
  } from "medusa-contact-us"
29
47
 
30
48
  const plugins = [
31
49
  {
32
50
  resolve: "medusa-contact-us",
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",
68
+ },
69
+ {
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,
87
+ },
88
+ {
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
94
+ },
95
+ {
96
+ from: "in_progress",
97
+ to: "resolved",
98
+ send_email: true,
99
+ email_subject: "Your request has been resolved",
100
+ },
101
+ {
102
+ from: "resolved",
103
+ to: "closed",
104
+ send_email: false,
105
+ },
106
+ ],
107
+ email: {
108
+ enabled: true,
109
+ default_subject: "Contact Request Status Update",
110
+ default_template: null, // Optional: default email template path
111
+ },
112
+ },
33
113
  },
34
114
  ]
35
115
 
36
- // Register the ContactSubscriptionModule
37
- const modules = [ContactSubscriptionModule]
116
+ // Register the modules
117
+ const modules = [ContactSubscriptionModule, ContactRequestModule]
38
118
 
39
119
  export default {
40
120
  projectConfig: {
@@ -47,10 +127,91 @@ export default {
47
127
  } satisfies ConfigModule
48
128
  ```
49
129
 
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
154
+
50
155
  ## REST API
51
156
 
52
157
  ### Storefront (open)
53
158
 
159
+ #### Contact Requests
160
+
161
+ Create a contact request:
162
+
163
+ ```bash
164
+ curl -X POST https://your-medusa.com/store/contact-requests \
165
+ -H "Content-Type: application/json" \
166
+ -H "x-publishable-api-key: pk_storefront" \
167
+ -d '{
168
+ "email": "customer@example.com",
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"
177
+ }'
178
+ ```
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
+
186
+ Response:
187
+
188
+ ```json
189
+ {
190
+ "request": {
191
+ "id": "creq_123",
192
+ "email": "customer@example.com",
193
+ "payload": {
194
+ "subject": "Order inquiry",
195
+ "message": "I need help with my order #12345"
196
+ },
197
+ "status": "pending",
198
+ "status_history": [
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"
211
+ }
212
+ }
213
+ ```
214
+
54
215
  #### Email subscriptions
55
216
 
56
217
  ```bash
@@ -85,6 +246,83 @@ Response:
85
246
 
86
247
  ### Admin (requires admin auth cookie/token)
87
248
 
249
+ #### Contact Requests
250
+
251
+ List contact requests:
252
+
253
+ ```bash
254
+ curl -X GET "https://your-medusa.com/admin/contact-requests?status=pending&email=customer@example.com&limit=20&offset=0" \
255
+ -H "Authorization: Bearer <token>"
256
+ ```
257
+
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:
270
+
271
+ ```bash
272
+ curl -X GET "https://your-medusa.com/admin/contact-requests/creq_123" \
273
+ -H "Authorization: Bearer <token>"
274
+ ```
275
+
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):
296
+
297
+ ```bash
298
+ curl -X POST https://your-medusa.com/admin/contact-requests \
299
+ -H "Content-Type: application/json" \
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
+ }'
309
+ ```
310
+
311
+ Update contact request status:
312
+
313
+ ```bash
314
+ curl -X POST https://your-medusa.com/admin/contact-requests/creq_123/status \
315
+ -H "Content-Type: application/json" \
316
+ -H "Authorization: Bearer <token>" \
317
+ -d '{
318
+ "status": "in_progress"
319
+ }'
320
+ ```
321
+
322
+ **Note**: Only status transitions defined in `status_transitions` configuration are allowed. The API will return an error if an invalid transition is attempted.
323
+
324
+ #### Email Subscriptions
325
+
88
326
  List subscriptions:
89
327
 
90
328
  ```bash
@@ -109,10 +347,88 @@ After running `medusa admin dev --plugins medusa-contact-us`, the sidebar will i
109
347
 
110
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.
111
349
 
350
+ ### Contact Requests
351
+
352
+ Submit a contact request from your storefront:
353
+
354
+ ```ts
355
+ import { submitContactRequest } from "medusa-contact-us/helpers"
356
+
357
+ const result = await submitContactRequest(
358
+ {
359
+ email: "customer@example.com",
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",
370
+ },
371
+ {
372
+ baseUrl: "https://store.myshop.com",
373
+ publishableApiKey: "pk_test_storefront",
374
+ }
375
+ )
376
+
377
+ console.log("Request created:", result.request.id)
378
+ ```
379
+
380
+ Using a Medusa JS client:
381
+
382
+ ```ts
383
+ import Medusa from "@medusajs/medusa-js"
384
+ import { submitContactRequest } from "medusa-contact-us/helpers"
385
+
386
+ const medusa = new Medusa({
387
+ baseUrl: "https://store.myshop.com",
388
+ publishableKey: "pk_live_client",
389
+ })
390
+
391
+ await submitContactRequest(
392
+ {
393
+ email: "customer@example.com",
394
+ payload: {
395
+ subject: "Support request",
396
+ message: "Need assistance",
397
+ },
398
+ },
399
+ {
400
+ client: medusa,
401
+ publishableApiKey: "pk_live_client",
402
+ }
403
+ )
404
+ ```
405
+
406
+ For SSR or edge runtimes, preconfigure the helper:
407
+
408
+ ```ts
409
+ import { createSubmitContactRequest } from "medusa-contact-us/helpers"
410
+
411
+ export const submitRequest = createSubmitContactRequest({
412
+ baseUrl: process.env.NEXT_PUBLIC_MEDUSA_URL,
413
+ publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
414
+ })
415
+
416
+ export async function action(formData: FormData) {
417
+ await submitRequest({
418
+ email: formData.get("email") as string,
419
+ payload: {
420
+ subject: formData.get("subject") as string,
421
+ message: formData.get("message") as string,
422
+ },
423
+ source: "contact_form",
424
+ })
425
+ }
426
+ ```
427
+
112
428
  ### Email subscriptions
113
429
 
114
430
  ```ts
115
- import { upsertContactSubscription } from "medusa-contact-us"
431
+ import { upsertContactSubscription } from "medusa-contact-us/helpers"
116
432
 
117
433
  await upsertContactSubscription(
118
434
  {
@@ -131,7 +447,7 @@ Using a Medusa JS client keeps credentials in one place while still letting you
131
447
 
132
448
  ```ts
133
449
  import Medusa from "@medusajs/medusa-js"
134
- import { upsertContactSubscription } from "medusa-contact-us"
450
+ import { upsertContactSubscription } from "medusa-contact-us/helpers"
135
451
 
136
452
  const medusa = new Medusa({
137
453
  baseUrl: "https://store.myshop.com",
@@ -156,7 +472,7 @@ await upsertContactSubscription(
156
472
  For SSR or edge runtimes, preconfigure the helper once:
157
473
 
158
474
  ```ts
159
- import { createUpsertContactSubscription } from "medusa-contact-us"
475
+ import { createUpsertContactSubscription } from "medusa-contact-us/helpers"
160
476
 
161
477
  export const upsertSubscription = createUpsertContactSubscription({
162
478
  baseUrl: process.env.NEXT_PUBLIC_MEDUSA_URL,
@@ -166,8 +482,8 @@ export const upsertSubscription = createUpsertContactSubscription({
166
482
  export async function action(formData: FormData) {
167
483
  await upsertSubscription({
168
484
  email: formData.get("email") as string,
169
- status: input.unsubscribe ? "unsubscribed" : "subscribed",
170
- source: input.source ?? "footer_form",
485
+ status: formData.get("unsubscribe") ? "unsubscribed" : "subscribed",
486
+ source: "footer_form",
171
487
  })
172
488
  }
173
489
  ```
@@ -182,6 +498,57 @@ export async function action(formData: FormData) {
182
498
 
183
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.
184
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
522
+
523
+ When fetching a contact request via the admin API, the response includes `next_allowed_statuses`:
524
+
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
+ ```
551
+
185
552
  ## Testing & build
186
553
 
187
554
  ```bash
@@ -193,4 +560,8 @@ Always run `yarn build` after development to ensure the bundler succeeds before
193
560
 
194
561
  ## Troubleshooting
195
562
 
196
- - 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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "medusa-contact-us",
3
- "version": "0.0.17",
3
+ "version": "0.0.21",
4
4
  "description": "Manage storefront email subscriptions (opt-ins and opt-outs) in Medusa Admin.",
5
5
  "author": "Medusa (https://medusajs.com)",
6
6
  "license": "MIT",
@@ -9,6 +9,7 @@
9
9
  ],
10
10
  "exports": {
11
11
  "./package.json": "./package.json",
12
+ "./workflows": "./.medusa/server/src/workflows/index.js",
12
13
  "./helpers": "./.medusa/server/src/helpers/index.js",
13
14
  "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
14
15
  "./modules/*": "./.medusa/server/src/modules/*/index.js",