@bernierllc/email-campaign-management 1.0.1

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 (118) hide show
  1. package/.eslintrc.cjs +45 -0
  2. package/README.md +316 -0
  3. package/__tests__/api/campaigns.test.ts +217 -0
  4. package/__tests__/api/client.test.ts +330 -0
  5. package/__tests__/components/CampaignBuilder.test.tsx +103 -0
  6. package/__tests__/components/CampaignDashboard.test.tsx +89 -0
  7. package/__tests__/components/CampaignList.test.tsx +144 -0
  8. package/__tests__/components/MetricsOverview.test.tsx +200 -0
  9. package/__tests__/components/PerformanceChart.test.tsx +206 -0
  10. package/__tests__/hooks/useCampaignStore.test.ts +450 -0
  11. package/__tests__/hooks/useWorkflowValidation.test.ts +176 -0
  12. package/__tests__/utils/formatting.test.ts +48 -0
  13. package/__tests__/utils/validation.test.ts +199 -0
  14. package/__tests__/utils/workflow-helpers.test.ts +134 -0
  15. package/coverage/clover.xml +314 -0
  16. package/coverage/coverage-final.json +16 -0
  17. package/coverage/lcov-report/base.css +224 -0
  18. package/coverage/lcov-report/block-navigation.js +87 -0
  19. package/coverage/lcov-report/favicon.png +0 -0
  20. package/coverage/lcov-report/index.html +221 -0
  21. package/coverage/lcov-report/prettify.css +1 -0
  22. package/coverage/lcov-report/prettify.js +2 -0
  23. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  24. package/coverage/lcov-report/sorter.js +210 -0
  25. package/coverage/lcov-report/src/api/campaigns.ts.html +199 -0
  26. package/coverage/lcov-report/src/api/client.ts.html +478 -0
  27. package/coverage/lcov-report/src/api/index.html +131 -0
  28. package/coverage/lcov-report/src/components/CampaignBuilder/index.html +116 -0
  29. package/coverage/lcov-report/src/components/CampaignBuilder/index.tsx.html +454 -0
  30. package/coverage/lcov-report/src/components/CampaignDashboard/MetricsOverview.tsx.html +208 -0
  31. package/coverage/lcov-report/src/components/CampaignDashboard/PerformanceChart.tsx.html +232 -0
  32. package/coverage/lcov-report/src/components/CampaignDashboard/index.html +146 -0
  33. package/coverage/lcov-report/src/components/CampaignDashboard/index.tsx.html +241 -0
  34. package/coverage/lcov-report/src/components/CampaignList/index.html +116 -0
  35. package/coverage/lcov-report/src/components/CampaignList/index.tsx.html +244 -0
  36. package/coverage/lcov-report/src/config.ts.html +202 -0
  37. package/coverage/lcov-report/src/hooks/index.html +146 -0
  38. package/coverage/lcov-report/src/hooks/useCampaignMetrics.ts.html +208 -0
  39. package/coverage/lcov-report/src/hooks/useCampaignStore.ts.html +343 -0
  40. package/coverage/lcov-report/src/hooks/useWorkflowValidation.ts.html +136 -0
  41. package/coverage/lcov-report/src/index.html +116 -0
  42. package/coverage/lcov-report/src/types/index.html +116 -0
  43. package/coverage/lcov-report/src/types/index.ts.html +127 -0
  44. package/coverage/lcov-report/src/utils/formatting.ts.html +163 -0
  45. package/coverage/lcov-report/src/utils/index.html +146 -0
  46. package/coverage/lcov-report/src/utils/validation.ts.html +394 -0
  47. package/coverage/lcov-report/src/utils/workflow-helpers.ts.html +277 -0
  48. package/coverage/lcov.info +657 -0
  49. package/dist/api/campaigns.d.ts +9 -0
  50. package/dist/api/campaigns.js +38 -0
  51. package/dist/api/client.d.ts +14 -0
  52. package/dist/api/client.js +116 -0
  53. package/dist/components/CampaignBuilder/index.d.ts +8 -0
  54. package/dist/components/CampaignBuilder/index.js +88 -0
  55. package/dist/components/CampaignDashboard/MetricsOverview.d.ts +6 -0
  56. package/dist/components/CampaignDashboard/MetricsOverview.js +34 -0
  57. package/dist/components/CampaignDashboard/PerformanceChart.d.ts +7 -0
  58. package/dist/components/CampaignDashboard/PerformanceChart.js +45 -0
  59. package/dist/components/CampaignDashboard/index.d.ts +5 -0
  60. package/dist/components/CampaignDashboard/index.js +44 -0
  61. package/dist/components/CampaignList/index.d.ts +6 -0
  62. package/dist/components/CampaignList/index.js +68 -0
  63. package/dist/config.d.ts +12 -0
  64. package/dist/config.js +31 -0
  65. package/dist/hooks/useCampaignMetrics.d.ts +2 -0
  66. package/dist/hooks/useCampaignMetrics.js +42 -0
  67. package/dist/hooks/useCampaignStore.d.ts +14 -0
  68. package/dist/hooks/useCampaignStore.js +105 -0
  69. package/dist/hooks/useWorkflowValidation.d.ts +3 -0
  70. package/dist/hooks/useWorkflowValidation.js +17 -0
  71. package/dist/index.d.ts +13 -0
  72. package/dist/index.js +52 -0
  73. package/dist/types/abtest.d.ts +15 -0
  74. package/dist/types/abtest.js +9 -0
  75. package/dist/types/audience.d.ts +18 -0
  76. package/dist/types/audience.js +9 -0
  77. package/dist/types/campaign.d.ts +31 -0
  78. package/dist/types/campaign.js +9 -0
  79. package/dist/types/index.d.ts +6 -0
  80. package/dist/types/index.js +29 -0
  81. package/dist/types/metrics.d.ts +27 -0
  82. package/dist/types/metrics.js +9 -0
  83. package/dist/types/schedule.d.ts +15 -0
  84. package/dist/types/schedule.js +9 -0
  85. package/dist/types/workflow.d.ts +37 -0
  86. package/dist/types/workflow.js +9 -0
  87. package/dist/utils/formatting.d.ts +4 -0
  88. package/dist/utils/formatting.js +28 -0
  89. package/dist/utils/validation.d.ts +8 -0
  90. package/dist/utils/validation.js +81 -0
  91. package/dist/utils/workflow-helpers.d.ts +12 -0
  92. package/dist/utils/workflow-helpers.js +62 -0
  93. package/jest.config.cjs +33 -0
  94. package/jest.setup.cjs +9 -0
  95. package/package.json +72 -0
  96. package/src/api/campaigns.ts +38 -0
  97. package/src/api/client.ts +131 -0
  98. package/src/components/CampaignBuilder/index.tsx +123 -0
  99. package/src/components/CampaignDashboard/MetricsOverview.tsx +41 -0
  100. package/src/components/CampaignDashboard/PerformanceChart.tsx +49 -0
  101. package/src/components/CampaignDashboard/index.tsx +52 -0
  102. package/src/components/CampaignList/index.tsx +53 -0
  103. package/src/config.ts +39 -0
  104. package/src/hooks/useCampaignMetrics.ts +41 -0
  105. package/src/hooks/useCampaignStore.ts +86 -0
  106. package/src/hooks/useWorkflowValidation.ts +17 -0
  107. package/src/index.ts +32 -0
  108. package/src/types/abtest.ts +25 -0
  109. package/src/types/audience.ts +30 -0
  110. package/src/types/campaign.ts +44 -0
  111. package/src/types/index.ts +14 -0
  112. package/src/types/metrics.ts +36 -0
  113. package/src/types/schedule.ts +26 -0
  114. package/src/types/workflow.ts +53 -0
  115. package/src/utils/formatting.ts +26 -0
  116. package/src/utils/validation.ts +103 -0
  117. package/src/utils/workflow-helpers.ts +64 -0
  118. package/tsconfig.json +24 -0
package/.eslintrc.cjs ADDED
@@ -0,0 +1,45 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ module.exports = {
10
+ parser: '@typescript-eslint/parser',
11
+ parserOptions: {
12
+ ecmaVersion: 2020,
13
+ sourceType: 'module',
14
+ ecmaFeatures: {
15
+ jsx: true
16
+ }
17
+ },
18
+ extends: [
19
+ 'eslint:recommended',
20
+ 'plugin:@typescript-eslint/recommended',
21
+ 'plugin:react/recommended',
22
+ 'plugin:react-hooks/recommended'
23
+ ],
24
+ plugins: ['@typescript-eslint', 'react', 'react-hooks'],
25
+ env: {
26
+ node: true,
27
+ es6: true,
28
+ browser: true
29
+ },
30
+ settings: {
31
+ react: {
32
+ version: 'detect'
33
+ }
34
+ },
35
+ rules: {
36
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
37
+ '@typescript-eslint/no-explicit-any': 'warn',
38
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
39
+ 'react/react-in-jsx-scope': 'off',
40
+ 'react/prop-types': 'off',
41
+ 'jsx-a11y/control-has-associated-label': 'off',
42
+ 'jsx-a11y/click-events-have-key-events': 'off',
43
+ 'jsx-a11y/no-noninteractive-element-interactions': 'off'
44
+ }
45
+ };
package/README.md ADDED
@@ -0,0 +1,316 @@
1
+ # @bernierllc/email-campaign-management
2
+
3
+ Marketing email campaign management UI with visual workflow builder, A/B testing, and real-time analytics.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bernierllc/email-campaign-management
9
+ ```
10
+
11
+ ## Peer Dependencies
12
+
13
+ This package requires React 18+ as a peer dependency:
14
+
15
+ ```bash
16
+ npm install react react-dom
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Basic Campaign Dashboard
22
+
23
+ ```tsx
24
+ import { CampaignDashboard } from '@bernierllc/email-campaign-management';
25
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
26
+
27
+ const queryClient = new QueryClient();
28
+
29
+ function App() {
30
+ return (
31
+ <QueryClientProvider client={queryClient}>
32
+ <CampaignDashboard campaignId="campaign-123" />
33
+ </QueryClientProvider>
34
+ );
35
+ }
36
+ ```
37
+
38
+ ### Campaign Builder
39
+
40
+ ```tsx
41
+ import { CampaignBuilder } from '@bernierllc/email-campaign-management';
42
+
43
+ function CreateCampaign() {
44
+ const handleSave = (campaign) => {
45
+ console.log('Campaign saved:', campaign);
46
+ // Save to your backend
47
+ };
48
+
49
+ return (
50
+ <CampaignBuilder
51
+ onSave={handleSave}
52
+ onPublish={(campaign) => console.log('Publishing:', campaign)}
53
+ />
54
+ );
55
+ }
56
+ ```
57
+
58
+ ### Campaign List
59
+
60
+ ```tsx
61
+ import { CampaignList } from '@bernierllc/email-campaign-management';
62
+
63
+ function MyCampaigns() {
64
+ return (
65
+ <CampaignList
66
+ onSelectCampaign={(campaign) => {
67
+ console.log('Selected:', campaign);
68
+ }}
69
+ />
70
+ );
71
+ }
72
+ ```
73
+
74
+ ### Using Campaign Store (Zustand)
75
+
76
+ ```tsx
77
+ import { useCampaignStore } from '@bernierllc/email-campaign-management';
78
+
79
+ function CampaignManager() {
80
+ const {
81
+ campaigns,
82
+ loading,
83
+ fetchCampaigns,
84
+ createCampaign,
85
+ updateCampaign
86
+ } = useCampaignStore();
87
+
88
+ useEffect(() => {
89
+ fetchCampaigns();
90
+ }, []);
91
+
92
+ const handleCreate = async () => {
93
+ await createCampaign({
94
+ name: 'New Campaign',
95
+ type: 'drip',
96
+ status: 'draft',
97
+ workflow: { id: 'w1', nodes: [], edges: [] },
98
+ audience: { type: 'all' },
99
+ schedule: { type: 'immediate' }
100
+ });
101
+ };
102
+
103
+ return (
104
+ <div>
105
+ {campaigns.map(campaign => (
106
+ <div key={campaign.id}>{campaign.name}</div>
107
+ ))}
108
+ </div>
109
+ );
110
+ }
111
+ ```
112
+
113
+ ### Workflow Validation
114
+
115
+ ```tsx
116
+ import { useWorkflowValidation } from '@bernierllc/email-campaign-management';
117
+ import type { CampaignWorkflow } from '@bernierllc/email-campaign-management';
118
+
119
+ function WorkflowEditor({ workflow }: { workflow: CampaignWorkflow }) {
120
+ const validation = useWorkflowValidation(workflow);
121
+
122
+ return (
123
+ <div>
124
+ {!validation.valid && (
125
+ <div className="errors">
126
+ {validation.errors.map((error, i) => (
127
+ <p key={i}>{error}</p>
128
+ ))}
129
+ </div>
130
+ )}
131
+ </div>
132
+ );
133
+ }
134
+ ```
135
+
136
+ ## API Reference
137
+
138
+ ### Components
139
+
140
+ #### `CampaignDashboard`
141
+
142
+ Displays campaign analytics and performance metrics.
143
+
144
+ **Props:**
145
+ - `campaignId: string` - ID of the campaign to display
146
+
147
+ #### `CampaignBuilder`
148
+
149
+ Campaign creation and editing interface with workflow builder.
150
+
151
+ **Props:**
152
+ - `campaign?: Campaign` - Existing campaign to edit (optional)
153
+ - `onSave: (campaign: Partial<Campaign>) => void` - Save handler
154
+ - `onPublish?: (campaign: Campaign) => void` - Publish handler (optional)
155
+
156
+ #### `CampaignList`
157
+
158
+ List of all campaigns with filtering and selection.
159
+
160
+ **Props:**
161
+ - `onSelectCampaign?: (campaign: Campaign) => void` - Selection handler (optional)
162
+
163
+ ### Hooks
164
+
165
+ #### `useCampaignStore()`
166
+
167
+ Zustand store for campaign state management.
168
+
169
+ **Returns:**
170
+ ```typescript
171
+ {
172
+ campaigns: Campaign[];
173
+ activeCampaign: Campaign | null;
174
+ loading: boolean;
175
+ error: string | null;
176
+ fetchCampaigns: () => Promise<void>;
177
+ createCampaign: (campaign: Partial<Campaign>) => Promise<Campaign | null>;
178
+ updateCampaign: (id: string, updates: Partial<Campaign>) => Promise<void>;
179
+ deleteCampaign: (id: string) => Promise<void>;
180
+ setActiveCampaign: (campaign: Campaign | null) => void;
181
+ }
182
+ ```
183
+
184
+ #### `useCampaignMetrics(campaignId: string)`
185
+
186
+ React Query hook for fetching campaign metrics.
187
+
188
+ **Returns:** Query result with `CampaignMetrics` data
189
+
190
+ #### `useEmailMetrics(campaignId: string)`
191
+
192
+ React Query hook for fetching email-level metrics.
193
+
194
+ **Returns:** Query result with `EmailMetrics[]` data
195
+
196
+ #### `useWorkflowValidation(workflow: CampaignWorkflow)`
197
+
198
+ Validates workflow structure.
199
+
200
+ **Returns:**
201
+ ```typescript
202
+ {
203
+ valid: boolean;
204
+ errors: string[];
205
+ }
206
+ ```
207
+
208
+ ### Types
209
+
210
+ ```typescript
211
+ interface Campaign {
212
+ id: string;
213
+ name: string;
214
+ description?: string;
215
+ type: 'one-time' | 'drip' | 'welcome' | 'nurture' | 'automated';
216
+ status: 'draft' | 'scheduled' | 'active' | 'paused' | 'completed';
217
+ workflow: CampaignWorkflow;
218
+ audience: AudienceConfig;
219
+ schedule: ScheduleConfig;
220
+ abTest?: ABTestConfig;
221
+ createdAt: Date;
222
+ updatedAt: Date;
223
+ createdBy: string;
224
+ }
225
+
226
+ interface CampaignMetrics {
227
+ campaignId: string;
228
+ sent: number;
229
+ delivered: number;
230
+ bounced: number;
231
+ opened: number;
232
+ clicked: number;
233
+ converted: number;
234
+ unsubscribed: number;
235
+ openRate: number;
236
+ clickRate: number;
237
+ conversionRate: number;
238
+ lastUpdated: Date;
239
+ }
240
+ ```
241
+
242
+ See full type definitions in [src/types](./src/types).
243
+
244
+ ## Configuration
245
+
246
+ ### Environment Variables
247
+
248
+ ```bash
249
+ # API Configuration
250
+ REACT_APP_API_URL=http://localhost:3000/api
251
+
252
+ # Feature Flags
253
+ REACT_APP_ENABLE_AB_TESTING=true
254
+ REACT_APP_ENABLE_ANALYTICS=true
255
+
256
+ # Metrics Configuration
257
+ REACT_APP_METRICS_REFRESH_INTERVAL=30000 # 30 seconds
258
+
259
+ # Workflow Limits
260
+ REACT_APP_MAX_WORKFLOW_NODES=50
261
+
262
+ # Defaults
263
+ REACT_APP_DEFAULT_TIMEZONE=America/New_York
264
+ ```
265
+
266
+ ### Runtime Configuration
267
+
268
+ ```tsx
269
+ import { setConfig } from '@bernierllc/email-campaign-management';
270
+
271
+ setConfig({
272
+ apiUrl: 'https://api.example.com',
273
+ enableABTesting: true,
274
+ metricsRefreshInterval: 60000
275
+ });
276
+ ```
277
+
278
+ ## Features
279
+
280
+ - **Campaign Builder**: Visual workflow editor for creating email sequences
281
+ - **A/B Testing**: Configure and monitor A/B test campaigns
282
+ - **Real-time Analytics**: Live campaign performance metrics
283
+ - **Audience Segmentation**: Target specific user segments
284
+ - **Campaign Scheduling**: Schedule campaigns or trigger based on events
285
+ - **Campaign Library**: Save, clone, and reuse campaign templates
286
+
287
+ ## Integration Status
288
+
289
+ - **Logger**: integrated - UI actions, errors, and performance logging
290
+ - **Docs-Suite**: ready - TypeDoc format
291
+ - **NeverHub**: optional - Real-time event updates and campaign coordination
292
+
293
+ ## Dependencies
294
+
295
+ This package integrates with:
296
+ - `@bernierllc/email-sender` - Email delivery
297
+ - `@bernierllc/email-webhook-events` - Event tracking
298
+ - `@bernierllc/logger` - Logging infrastructure
299
+
300
+ ## Architecture Notes
301
+
302
+ **CRITICAL DEPENDENCY**: This package requires `@bernierllc/workflow-ui` from the content-suite, which provides the visual workflow builder component. Until that package is published, the workflow canvas will show a placeholder.
303
+
304
+ ## Examples
305
+
306
+ See the [examples](./examples) directory for complete working examples:
307
+ - Basic campaign creation
308
+ - A/B test setup
309
+ - Analytics dashboard integration
310
+ - Custom workflow builders
311
+
312
+ ## License
313
+
314
+ Copyright (c) 2025 Bernier LLC. All rights reserved.
315
+
316
+ This file is licensed to the client under a limited-use license. The client may use and modify this code only within the scope of the project it was delivered for. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
@@ -0,0 +1,217 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import {
10
+ getCampaigns,
11
+ getCampaign,
12
+ createCampaign,
13
+ updateCampaign,
14
+ deleteCampaign,
15
+ getCampaignMetrics,
16
+ getEmailMetrics
17
+ } from '../../src/api/campaigns';
18
+ import { apiClient } from '../../src/api/client';
19
+ import { CampaignType } from '../../src/types';
20
+
21
+ jest.mock('../../src/api/client', () => ({
22
+ apiClient: {
23
+ get: jest.fn(),
24
+ post: jest.fn(),
25
+ put: jest.fn(),
26
+ delete: jest.fn()
27
+ }
28
+ }));
29
+
30
+ const mockApiClient = apiClient as jest.Mocked<typeof apiClient>;
31
+
32
+ describe('campaigns API', () => {
33
+ beforeEach(() => {
34
+ jest.clearAllMocks();
35
+ });
36
+
37
+ describe('getCampaigns', () => {
38
+ it('should call apiClient.get with correct endpoint', async () => {
39
+ const mockResponse = { success: true, data: [] };
40
+ mockApiClient.get.mockResolvedValueOnce(mockResponse);
41
+
42
+ const result = await getCampaigns();
43
+
44
+ expect(mockApiClient.get).toHaveBeenCalledWith('/campaigns');
45
+ expect(result).toEqual(mockResponse);
46
+ });
47
+
48
+ it('should return error response on failure', async () => {
49
+ const mockError = { success: false, error: 'Failed to fetch' };
50
+ mockApiClient.get.mockResolvedValueOnce(mockError);
51
+
52
+ const result = await getCampaigns();
53
+
54
+ expect(result).toEqual(mockError);
55
+ });
56
+ });
57
+
58
+ describe('getCampaign', () => {
59
+ it('should call apiClient.get with campaign ID', async () => {
60
+ const mockResponse = { success: true, data: { id: 'campaign-123' } };
61
+ mockApiClient.get.mockResolvedValueOnce(mockResponse);
62
+
63
+ const result = await getCampaign('campaign-123');
64
+
65
+ expect(mockApiClient.get).toHaveBeenCalledWith('/campaigns/campaign-123');
66
+ expect(result).toEqual(mockResponse);
67
+ });
68
+
69
+ it('should handle non-existent campaign', async () => {
70
+ const mockError = { success: false, error: 'Campaign not found' };
71
+ mockApiClient.get.mockResolvedValueOnce(mockError);
72
+
73
+ const result = await getCampaign('nonexistent');
74
+
75
+ expect(result).toEqual(mockError);
76
+ });
77
+ });
78
+
79
+ describe('createCampaign', () => {
80
+ it('should call apiClient.post with campaign data', async () => {
81
+ const campaignData = { name: 'New Campaign', type: 'one-time' as CampaignType };
82
+ const mockResponse = { success: true, data: { id: 'new-id', ...campaignData } };
83
+ mockApiClient.post.mockResolvedValueOnce(mockResponse);
84
+
85
+ const result = await createCampaign(campaignData);
86
+
87
+ expect(mockApiClient.post).toHaveBeenCalledWith('/campaigns', campaignData);
88
+ expect(result).toEqual(mockResponse);
89
+ });
90
+
91
+ it('should handle validation errors', async () => {
92
+ const mockError = { success: false, error: 'Name is required' };
93
+ mockApiClient.post.mockResolvedValueOnce(mockError);
94
+
95
+ const result = await createCampaign({});
96
+
97
+ expect(result).toEqual(mockError);
98
+ });
99
+ });
100
+
101
+ describe('updateCampaign', () => {
102
+ it('should call apiClient.put with campaign ID and updates', async () => {
103
+ const updates = { name: 'Updated Name' };
104
+ const mockResponse = { success: true, data: { id: 'campaign-123', name: 'Updated Name' } };
105
+ mockApiClient.put.mockResolvedValueOnce(mockResponse);
106
+
107
+ const result = await updateCampaign('campaign-123', updates);
108
+
109
+ expect(mockApiClient.put).toHaveBeenCalledWith('/campaigns/campaign-123', updates);
110
+ expect(result).toEqual(mockResponse);
111
+ });
112
+
113
+ it('should handle update failures', async () => {
114
+ const mockError = { success: false, error: 'Update failed' };
115
+ mockApiClient.put.mockResolvedValueOnce(mockError);
116
+
117
+ const result = await updateCampaign('campaign-123', { name: '' });
118
+
119
+ expect(result).toEqual(mockError);
120
+ });
121
+ });
122
+
123
+ describe('deleteCampaign', () => {
124
+ it('should call apiClient.delete with campaign ID', async () => {
125
+ const mockResponse = { success: true };
126
+ mockApiClient.delete.mockResolvedValueOnce(mockResponse);
127
+
128
+ const result = await deleteCampaign('campaign-123');
129
+
130
+ expect(mockApiClient.delete).toHaveBeenCalledWith('/campaigns/campaign-123');
131
+ expect(result).toEqual(mockResponse);
132
+ });
133
+
134
+ it('should handle delete failures', async () => {
135
+ const mockError = { success: false, error: 'Cannot delete active campaign' };
136
+ mockApiClient.delete.mockResolvedValueOnce(mockError);
137
+
138
+ const result = await deleteCampaign('active-campaign');
139
+
140
+ expect(result).toEqual(mockError);
141
+ });
142
+ });
143
+
144
+ describe('getCampaignMetrics', () => {
145
+ it('should call apiClient.get with metrics endpoint', async () => {
146
+ const mockMetrics = {
147
+ success: true,
148
+ data: {
149
+ campaignId: 'campaign-123',
150
+ sent: 1000,
151
+ delivered: 950,
152
+ bounced: 50,
153
+ opened: 400,
154
+ clicked: 100,
155
+ converted: 20,
156
+ unsubscribed: 5,
157
+ openRate: 40,
158
+ clickRate: 10,
159
+ conversionRate: 2,
160
+ lastUpdated: new Date()
161
+ }
162
+ };
163
+ mockApiClient.get.mockResolvedValueOnce(mockMetrics);
164
+
165
+ const result = await getCampaignMetrics('campaign-123');
166
+
167
+ expect(mockApiClient.get).toHaveBeenCalledWith('/campaigns/campaign-123/metrics');
168
+ expect(result).toEqual(mockMetrics);
169
+ });
170
+
171
+ it('should handle metrics not available', async () => {
172
+ const mockError = { success: false, error: 'Metrics not available yet' };
173
+ mockApiClient.get.mockResolvedValueOnce(mockError);
174
+
175
+ const result = await getCampaignMetrics('new-campaign');
176
+
177
+ expect(result).toEqual(mockError);
178
+ });
179
+ });
180
+
181
+ describe('getEmailMetrics', () => {
182
+ it('should call apiClient.get with email metrics endpoint', async () => {
183
+ const mockMetrics = {
184
+ success: true,
185
+ data: [
186
+ { emailId: 'email-1', opened: true, clicked: false },
187
+ { emailId: 'email-2', opened: true, clicked: true }
188
+ ]
189
+ };
190
+ mockApiClient.get.mockResolvedValueOnce(mockMetrics);
191
+
192
+ const result = await getEmailMetrics('campaign-123');
193
+
194
+ expect(mockApiClient.get).toHaveBeenCalledWith('/campaigns/campaign-123/emails/metrics');
195
+ expect(result).toEqual(mockMetrics);
196
+ });
197
+
198
+ it('should handle no emails sent yet', async () => {
199
+ const mockResponse = { success: true, data: [] };
200
+ mockApiClient.get.mockResolvedValueOnce(mockResponse);
201
+
202
+ const result = await getEmailMetrics('draft-campaign');
203
+
204
+ expect(result.success).toBe(true);
205
+ expect(result.data).toEqual([]);
206
+ });
207
+
208
+ it('should handle fetch failure', async () => {
209
+ const mockError = { success: false, error: 'Service unavailable' };
210
+ mockApiClient.get.mockResolvedValueOnce(mockError);
211
+
212
+ const result = await getEmailMetrics('campaign-123');
213
+
214
+ expect(result).toEqual(mockError);
215
+ });
216
+ });
217
+ });