@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.
- package/.eslintrc.cjs +45 -0
- package/README.md +316 -0
- package/__tests__/api/campaigns.test.ts +217 -0
- package/__tests__/api/client.test.ts +330 -0
- package/__tests__/components/CampaignBuilder.test.tsx +103 -0
- package/__tests__/components/CampaignDashboard.test.tsx +89 -0
- package/__tests__/components/CampaignList.test.tsx +144 -0
- package/__tests__/components/MetricsOverview.test.tsx +200 -0
- package/__tests__/components/PerformanceChart.test.tsx +206 -0
- package/__tests__/hooks/useCampaignStore.test.ts +450 -0
- package/__tests__/hooks/useWorkflowValidation.test.ts +176 -0
- package/__tests__/utils/formatting.test.ts +48 -0
- package/__tests__/utils/validation.test.ts +199 -0
- package/__tests__/utils/workflow-helpers.test.ts +134 -0
- package/coverage/clover.xml +314 -0
- package/coverage/coverage-final.json +16 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +221 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/api/campaigns.ts.html +199 -0
- package/coverage/lcov-report/src/api/client.ts.html +478 -0
- package/coverage/lcov-report/src/api/index.html +131 -0
- package/coverage/lcov-report/src/components/CampaignBuilder/index.html +116 -0
- package/coverage/lcov-report/src/components/CampaignBuilder/index.tsx.html +454 -0
- package/coverage/lcov-report/src/components/CampaignDashboard/MetricsOverview.tsx.html +208 -0
- package/coverage/lcov-report/src/components/CampaignDashboard/PerformanceChart.tsx.html +232 -0
- package/coverage/lcov-report/src/components/CampaignDashboard/index.html +146 -0
- package/coverage/lcov-report/src/components/CampaignDashboard/index.tsx.html +241 -0
- package/coverage/lcov-report/src/components/CampaignList/index.html +116 -0
- package/coverage/lcov-report/src/components/CampaignList/index.tsx.html +244 -0
- package/coverage/lcov-report/src/config.ts.html +202 -0
- package/coverage/lcov-report/src/hooks/index.html +146 -0
- package/coverage/lcov-report/src/hooks/useCampaignMetrics.ts.html +208 -0
- package/coverage/lcov-report/src/hooks/useCampaignStore.ts.html +343 -0
- package/coverage/lcov-report/src/hooks/useWorkflowValidation.ts.html +136 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/types/index.html +116 -0
- package/coverage/lcov-report/src/types/index.ts.html +127 -0
- package/coverage/lcov-report/src/utils/formatting.ts.html +163 -0
- package/coverage/lcov-report/src/utils/index.html +146 -0
- package/coverage/lcov-report/src/utils/validation.ts.html +394 -0
- package/coverage/lcov-report/src/utils/workflow-helpers.ts.html +277 -0
- package/coverage/lcov.info +657 -0
- package/dist/api/campaigns.d.ts +9 -0
- package/dist/api/campaigns.js +38 -0
- package/dist/api/client.d.ts +14 -0
- package/dist/api/client.js +116 -0
- package/dist/components/CampaignBuilder/index.d.ts +8 -0
- package/dist/components/CampaignBuilder/index.js +88 -0
- package/dist/components/CampaignDashboard/MetricsOverview.d.ts +6 -0
- package/dist/components/CampaignDashboard/MetricsOverview.js +34 -0
- package/dist/components/CampaignDashboard/PerformanceChart.d.ts +7 -0
- package/dist/components/CampaignDashboard/PerformanceChart.js +45 -0
- package/dist/components/CampaignDashboard/index.d.ts +5 -0
- package/dist/components/CampaignDashboard/index.js +44 -0
- package/dist/components/CampaignList/index.d.ts +6 -0
- package/dist/components/CampaignList/index.js +68 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +31 -0
- package/dist/hooks/useCampaignMetrics.d.ts +2 -0
- package/dist/hooks/useCampaignMetrics.js +42 -0
- package/dist/hooks/useCampaignStore.d.ts +14 -0
- package/dist/hooks/useCampaignStore.js +105 -0
- package/dist/hooks/useWorkflowValidation.d.ts +3 -0
- package/dist/hooks/useWorkflowValidation.js +17 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +52 -0
- package/dist/types/abtest.d.ts +15 -0
- package/dist/types/abtest.js +9 -0
- package/dist/types/audience.d.ts +18 -0
- package/dist/types/audience.js +9 -0
- package/dist/types/campaign.d.ts +31 -0
- package/dist/types/campaign.js +9 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +29 -0
- package/dist/types/metrics.d.ts +27 -0
- package/dist/types/metrics.js +9 -0
- package/dist/types/schedule.d.ts +15 -0
- package/dist/types/schedule.js +9 -0
- package/dist/types/workflow.d.ts +37 -0
- package/dist/types/workflow.js +9 -0
- package/dist/utils/formatting.d.ts +4 -0
- package/dist/utils/formatting.js +28 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.js +81 -0
- package/dist/utils/workflow-helpers.d.ts +12 -0
- package/dist/utils/workflow-helpers.js +62 -0
- package/jest.config.cjs +33 -0
- package/jest.setup.cjs +9 -0
- package/package.json +72 -0
- package/src/api/campaigns.ts +38 -0
- package/src/api/client.ts +131 -0
- package/src/components/CampaignBuilder/index.tsx +123 -0
- package/src/components/CampaignDashboard/MetricsOverview.tsx +41 -0
- package/src/components/CampaignDashboard/PerformanceChart.tsx +49 -0
- package/src/components/CampaignDashboard/index.tsx +52 -0
- package/src/components/CampaignList/index.tsx +53 -0
- package/src/config.ts +39 -0
- package/src/hooks/useCampaignMetrics.ts +41 -0
- package/src/hooks/useCampaignStore.ts +86 -0
- package/src/hooks/useWorkflowValidation.ts +17 -0
- package/src/index.ts +32 -0
- package/src/types/abtest.ts +25 -0
- package/src/types/audience.ts +30 -0
- package/src/types/campaign.ts +44 -0
- package/src/types/index.ts +14 -0
- package/src/types/metrics.ts +36 -0
- package/src/types/schedule.ts +26 -0
- package/src/types/workflow.ts +53 -0
- package/src/utils/formatting.ts +26 -0
- package/src/utils/validation.ts +103 -0
- package/src/utils/workflow-helpers.ts +64 -0
- 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
|
+
});
|