Skip to content

feat: Campaign Management System with CRUD, Analytics Dashboard, Market Intelligence Agent, and React Frontend#142

Open
devin-ai-integration[bot] wants to merge 54 commits into
mainfrom
devin/1777937680-campaign-management-system
Open

feat: Campaign Management System with CRUD, Analytics Dashboard, Market Intelligence Agent, and React Frontend#142
devin-ai-integration[bot] wants to merge 54 commits into
mainfrom
devin/1777937680-campaign-management-system

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented May 5, 2026

Copy link
Copy Markdown

Summary

Full-featured Campaign Management System with integrated Market Intelligence Agent, built on Java Spring Boot backend and React 18 + TypeScript + Nx frontend with Fiserv Admin Tool branding.

5-Step Campaign Creation Wizard (NEW)

  • Step 1 - Setup: Campaign name, description, keywords, product category dropdown, priority dropdown, start/end dates, visual channel selector cards (In-app, Email, SMS, Social Media, Ads)
  • Step 2 - Content: Multi-asset builder with Image/Text/HTML type selection, file upload dropzone, rich text editor, MCM media integration
  • Step 3 - Segment: User segment selector modal with searchable grid (24 segments), selected chips with X to remove, estimated reach calculation
  • Step 4 - Location: Collapsible page sections (Account summary, Make a transfer, Payments, Bill pay) with Website/Mobile placement checkboxes
  • Step 5 - Review: Read-only summary with edit pencil icons, Preview campaign button, Send for approval, Define journey actions
  • Step indicator with visual checkmarks for completed steps and clickable navigation

Enhanced Sidebar Navigation

  • Marketing section: Create campaign, Campaigns, Campaign journey, Locations, Analytics, User segment, Segment criteria, MCM media
  • Intelligence section: Market Research, Internal preview
  • Administration, Configuration, Reports (collapsible sections)
  • Entity selector at top and bottom of sidebar, breadcrumb navigation

Campaign Management (Stories 1-4)

  • Story 1 - CRUD: Create/Read/Update/Delete with lifecycle (DRAFT -> ACTIVE -> PAUSED -> ENDED), Marketing entitlement gate
  • Story 2 - Dashboard: 6 KPIs, filters, CSV export, pie/bar charts, demographic commonality
  • Story 3 - Personalization: Audience segmentation, personalization tokens, scheduling, display placement, frequency capping
  • Story 4 - Fulfillment: Remind-later deferral, workflow URL, decline suppression, confirmation feedback

Industry Features (8 total)

A/B Testing, Campaign Cloning, Tags & Categories, Multi-Channel Delivery, Priority System, Audit Trail, Calendar View, Bulk Actions

Market Intelligence Agent (Integrated)

Python FastAPI agentic research loop with React UI integrated into sidebar. Scrapes competitor sites, extracts evidence across 8 categories, detects gaps, generates campaign angles with confidence scoring.

Security & Quality

SSRF protection, CSV formula injection defense, SQLite connection leak prevention, async event loop protection, 10 rounds of Devin Review findings addressed.

Review & Testing Checklist for Human

  • Start Spring Boot backend (./gradlew bootRun) and login with janesmith / C@mp4ign!Mngr#2026
  • Navigate to Create campaign and verify the 5-step wizard flows correctly (Setup -> Content -> Segment -> Location -> Review)
  • In Step 3, open the segment modal, search and select segments, verify chips and estimated reach display
  • In Step 4, expand/collapse location sections, toggle website/mobile checkboxes
  • In Step 5, verify all entered data displays in review, test edit pencil icons jump back to correct steps
  • Test sidebar navigation to new pages (Campaign Journey, Locations, User Segment, Segment Criteria, MCM Media)
  • Verify bobsmith user sees Access Denied on all protected pages

Notes

  • The wizard replaces the old single-page campaign form at /campaigns/new. The old form is preserved at /campaigns/:id/edit for editing existing campaigns.
  • Intel Agent Python backend runs on port 8000 separately; Vite proxies /api/research to it.
  • Complex password: C@mp4ign!Mngr#2026

Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/3b231549f81347d89a211ebf5a75ee87


Open in Devin Review

…ard, and React frontend

Backend (Java Spring Boot):
- Campaign domain model with lifecycle management (DRAFT/ACTIVE/PAUSED/ENDED)
- CRUD REST API at /api/campaigns with entitlement-based access control
- Campaign decision tracking (ACCEPTED/DECLINED/CLICKED_UNFINISHED)
- Analytics endpoints with demographic commonality (segment, age group, region)
- Dashboard summary endpoint aggregating all campaign metrics
- MyBatis mappers and repository implementations
- Database migrations (V3 tables, V4 seed data)
- Unit tests for CampaignsApi controller (9 test cases)

Frontend (React 18 + TypeScript + Nx):
- Login page with JWT authentication
- Campaign list view with status filters and indicators
- Campaign create/edit forms with status-based field restrictions
- Campaign detail page with lifecycle actions (activate, pause, end, archive)
- Campaign analytics page with Recharts visualizations (pie, bar charts)
- Dashboard with KPIs, performance metrics, and recent campaigns
- Access denied screen for non-Marketing entitled users
- Playwright e2e test setup
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

…0, block ENDED campaign editing in frontend

- Add InvalidCampaignStateException with @ResponseStatus(CONFLICT) for proper 409 responses
- Replace IllegalStateException throws in CampaignService with InvalidCampaignStateException
- Wrap domain-level IllegalStateException from activate/pause with proper exception
- Add ENDED campaign guard in CampaignFormPage showing cannot-edit message
- Hide Edit button on CampaignDetailPage for ENDED campaigns
devin-ai-integration[bot]

This comment was marked as resolved.

… nav in DashboardPage

- Campaign.end() now validates current status is ACTIVE or PAUSED before transitioning
- DashboardPage Recent Campaigns table uses React Router Link instead of <a href>
devin-ai-integration[bot]

This comment was marked as resolved.

…ete/status actions

- findByStatus now accepts includeArchived param, matching findAll behavior
- CampaignMapper.xml uses dynamic <if> for archived filter in findByStatus query
- CampaignListPage.handleDelete wrapped in try/catch with user feedback
- CampaignDetailPage.handleStatusChange and handleDelete wrapped in try/catch
devin-ai-integration[bot]

This comment was marked as resolved.

…(N+1 -> 2 queries)

- app.spec.tsx: Add AuthProvider wrapper, update assertion to check login page
- Add countAllByDecisionForNonArchived aggregate SQL query
- Replace N+1 loop in getDashboardSummary with single aggregate query
- Dashboard now uses 2 queries instead of 4N+1
devin-ai-integration[bot]

This comment was marked as resolved.

…FK and date clearing bugs

- Redesign layout: dark blue gradient header bar, white left sidebar with
  emoji navigation icons, light gray background, breadcrumb navigation
- Update login page: themed header bar, dismissible error banner with
  warning icon and close button
- Update all page components (Dashboard, CampaignList, CampaignDetail,
  CampaignForm, CampaignAnalytics, AccessDenied) with Fiserv color scheme:
  #1a2744 navy headings, #1d4ed8 primary blue, #6366f1 secondary,
  #e5e7eb card borders, 0 1px 3px rgba(0,0,0,0.06) shadows
- Fix FK constraint violation: delete campaign_decisions before removing
  DRAFT campaigns (add deleteByCampaignId to repository chain)
- Fix date clearing: handle empty string dates to allow clearing
  previously-set start/end dates via clearStartDate/clearEndDate methods
devin-ai-integration[bot]

This comment was marked as resolved.

- Fix e2e test: h1 asserts 'Welcome Back' instead of 'Campaign Manager'
- CampaignsApi.listCampaigns: catch IllegalArgumentException on invalid
  status query param, return 400 instead of 500
- CampaignService.updateCampaign: move valueOf inside try-catch, catch
  IllegalArgumentException for invalid status strings
devin-ai-integration[bot]

This comment was marked as resolved.

- Wrap FulfillmentActionType.valueOf() in try-catch in createCampaign
  and updateCampaign, returning 409 instead of 500 on invalid values
- Change @NotNull to @notblank on fulfillmentActionType to reject
  empty strings at validation time
- Force UTC timezone in parseDate() to prevent date drift on
  non-UTC JVMs: withZoneUTC() on ISODateTimeFormat parser
…lters

Story 2: Dashboard filters (name, date, segment, decision type), CSV export,
  Remind Me Later as separate KPI, last-updated timestamp
Story 3: Personalization tokens, display placement, frequency capping,
  delivery window scheduling, audience segmentation rules
Story 4: Remind-later deferral config, fulfillment workflow URL,
  decline suppression, confirmation feedback message

Backend: Extended Campaign entity, V5 migration, updated MyBatis mappers,
  CampaignsApi CSV export endpoint, DashboardSummary with remindLater count
Frontend: Updated form, detail, dashboard, analytics pages with all new fields
devin-ai-integration[bot]

This comment was marked as resolved.

- filterDecision now filters campaigns by fulfillmentActionType
- Date filters use .slice(0,10) to compare date-only portions
- Results count badge includes filterDecision in condition
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

…izer for DateTime fields

- recordDecision now returns 409 instead of 500 for invalid decision types
- campaignToMap/decisionToMap put DateTime objects directly so Jackson's
  DateTimeSerializer handles UTC normalization consistently
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Sanitize values starting with =, +, -, @, tab, or carriage return
by prefixing with a single quote to prevent spreadsheet formula
injection when CSV is opened in Excel/Google Sheets/LibreOffice.
@devin-ai-integration devin-ai-integration Bot changed the title feat: Campaign Management System with CRUD, Analytics Dashboard, and React Frontend feat: Campaign Management System with CRUD, Analytics Dashboard, Market Intelligence Agent, and React Frontend May 5, 2026
Disabled follow_redirects and manually follow up to 5 redirects,
re-validating each redirect target through _is_safe_url() before
fetching. This prevents attackers from using an initial public URL
that redirects to internal/metadata endpoints.
devin-ai-integration[bot]

This comment was marked as resolved.

- Resolve DNS once via async _resolve_safe() using asyncio.to_thread
  to avoid blocking the event loop
- Pin URL to resolved IP via _pin_url(), setting Host header to
  original hostname — eliminates TOCTOU gap between DNS check and
  HTTP request
- Same pinning applied to each redirect hop
devin-ai-integration[bot]

This comment was marked as resolved.

IP-pinning rewrote URLs to connect to resolved IPs directly, but this
broke TLS certificate validation (SNI mismatch). Now uses the original
URL (preserving hostname for TLS/SNI) with pre-flight DNS validation
via _validate_url() and a short 5s timeout to minimize the rebinding
window. Redirects are still validated at each hop.
devin-ai-integration[bot]

This comment was marked as resolved.

Modifying an already-applied Flyway migration causes checksum
validation failure on existing deployments. Reverted V2 to its
original content and created V7__update_seed_passwords.sql with
UPDATE statements to change passwords to the complex password.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 42 additional findings in Devin Review.

Open in Devin Review

Comment on lines +1 to +2
-- Update seed user passwords to complex password: C@mp4ign!Mngr#2026
-- BCrypt hash: $2a$10$LUtBa47o7pSr8/JUK2bx7.ZzTPpzcX9C7eDwjIR6IPGtYr4GpewxS

@devin-ai-integration devin-ai-integration Bot May 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 V7 migration changes seed user passwords to undocumented hash, breaking login

The V7__update_seed_passwords.sql migration replaces all seed user passwords with a new BCrypt hash ($2a$10$LUtBa47o7pSr8/JUK2bx7...) but the comment on line 2 says "BCrypt hash for the new password:" without ever specifying what the actual plaintext password is. The README.md:50-53 still documents password123 as the credential for all three seed users, which no longer works after this migration runs. This makes the entire application unusable for development — the campaign admin login page (campaign-admin/src/pages/LoginPage.tsx) cannot authenticate, and no developer can determine the correct password without cracking the BCrypt hash.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional for this demo/workshop application — the original V2 migration also documents the plaintext password in a comment (-- Password for all users is: password123) so developers know the login credentials. These are seed users for a local dev environment, not production credentials. Removing the comment would make it harder for workshop participants to use the app.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ed24864. Removed the plaintext password from the SQL comment. The comment now reads -- Update seed user passwords to complex credentials without exposing the actual password.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a demo/seed-data application — the password was intentionally set by the user and is documented in the README. The migration comment now points to the README for credentials (-- Update seed user passwords (see README for credentials)), keeping the plaintext out of the SQL file while ensuring developers can find it. Fixed in 62d7bb9.

- Step 1 (Setup): Campaign name, description, keywords, product category, priority, dates, channel cards (In-app/Email/SMS/Social/Ads)
- Step 2 (Content): Multi-asset builder with Image/Text/HTML types, file upload, rich text editor
- Step 3 (Segment): User segment selector modal with searchable grid, chips, estimated reach
- Step 4 (Location): Collapsible page sections (Account summary, Make a transfer, Payments, Bill pay) with Website/Mobile placement checkboxes
- Step 5 (Review): Read-only summary with edit icons, Preview/Send for approval/Define journey buttons
- Updated sidebar navigation with full Fiserv Admin Tool menu structure
- Added placeholder pages for new nav items (Campaign Journey, Locations, User Segment, etc.)
- Entity selector and breadcrumb navigation
devin-ai-integration[bot]

This comment was marked as resolved.

- Map wizard priority labels to proper 1-10 scale (Low=2, Medium=5, High=8, Critical=10) matching existing seed data
- Add SOCIAL and ADS to calendar page channel labels with fallback for unknown channels
devin-ai-integration[bot]

This comment was marked as resolved.

…lers

All async endpoint handlers now use await asyncio.to_thread() for DB calls,
matching the pattern already used in _run_research background task.
devin-ai-integration[bot]

This comment was marked as resolved.

- Add \n to CSV escapeCsv dangerous prefix character set
- Eliminate SSRF TOCTOU by resolving DNS once and connecting to validated IP directly with Host header, preventing DNS rebinding attacks
devin-ai-integration[bot]

This comment was marked as resolved.

- Use original URL (not IP-based resp.url) for resolving relative redirects
- Restrict CORS origins to known frontend ports (configurable via CORS_ORIGINS env)
- Add rate limiting: max 10 research requests/min per IP on POST /api/research
devin-ai-integration[bot]

This comment was marked as resolved.

… chain tracking, evict stale rate-limit keys

- Revert IP-based fetch that broke TLS cert verification on HTTPS sites
- Use hostname-based URL with pre-validation + 5s timeout for SSRF defense
- Track current_url through redirect chain for correct relative Location resolution
- Evict empty IP keys from rate limiter dict to prevent unbounded memory growth
devin-ai-integration[bot]

This comment was marked as resolved.

…aintext password from migration

- Add SOCIAL and ADS to channelLabels in CampaignDetailPage and CampaignListPage
- Add SOCIAL and ADS options to channel select in CampaignFormPage
- Remove plaintext password from V7 migration SQL comment
devin-ai-integration[bot]

This comment was marked as resolved.

…tion comment

- Replace .replace('_', ' ') with .replace(/_/g, ' ') for multi-underscore action names
- Point migration comment to README for credentials
devin-ai-integration[bot]

This comment was marked as resolved.

…ry growth

Sweep all stale IP entries when dict exceeds 1000 keys, bounding memory
regardless of whether individual IPs make repeat requests.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 41 additional findings in Devin Review.

Open in Devin Review

Comment on lines +260 to +264
String window =
(c.getDeliveryStartTime() != null ? c.getDeliveryStartTime() : "")
+ "-"
+ (c.getDeliveryEndTime() != null ? c.getDeliveryEndTime() : "");
csv.append(escapeCsv(window)).append(',');

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 CSV export corrupts delivery window data when start time is null due to formula injection defense triggering on '-' prefix

In CampaignsApi.java:260-264, the delivery window string is constructed as (startTime ?? "") + "-" + (endTime ?? ""). When deliveryStartTime is null (common for campaigns without time restrictions), the result starts with - (e.g., "-20:00" or just "-"). The escapeCsv method at line 279 treats leading - as a potential CSV formula injection character and prepends a ' — turning the value into '-20:00 or '- in the exported CSV. This silently corrupts exported data for any campaign without a delivery start time configured.

Suggested change
String window =
(c.getDeliveryStartTime() != null ? c.getDeliveryStartTime() : "")
+ "-"
+ (c.getDeliveryEndTime() != null ? c.getDeliveryEndTime() : "");
csv.append(escapeCsv(window)).append(',');
String startTime = c.getDeliveryStartTime() != null ? c.getDeliveryStartTime() : "";
String endTime = c.getDeliveryEndTime() != null ? c.getDeliveryEndTime() : "";
String window = startTime.isEmpty() && endTime.isEmpty() ? "" :
startTime + "-" + endTime;
csv.append(escapeCsv(window)).append(',');
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0cff880. The delivery window now outputs an empty string when both start and end times are null, avoiding the - prefix that triggered the formula injection defense.

Avoid producing '-endTime' string that triggers formula injection defense.
Output empty string when both times are null.
devin-ai-integration[bot]

This comment was marked as resolved.

…teParam

Without it, Jackson cannot populate fields via deserialization since
UNWRAP_ROOT_VALUE is enabled and there are no setters.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants