Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ jobs:
# keeps rendering a value the list no longer offers. None of the three
# was gated before — the shape guard shipped with CRM-442 and never ran
# here. The bedrock lifecycle spec stays out: it reaches the network.
# - events manifest catalog (CRM-316): the mirror of the evo-flow event
# catalog the journey trigger and segment editor read. A drift (name,
# category, field type) only shows as an event the UI cannot pick.
# The whole list runs in ~40s and is deterministic.
#
# Running the whole suite here was tried and reverted: 160 jsdom files in
Expand Down Expand Up @@ -180,7 +183,8 @@ jobs:
src/contexts/chat/ConversationsContext.spec.tsx \
src/components/ai_agents/__tests__/ModelSelector.availableModels.spec.ts \
src/components/ai_agents/__tests__/ModelSelector.retirement.spec.ts \
src/components/ai_agents/__tests__/ModelSelector.retiredValue.spec.tsx
src/components/ai_agents/__tests__/ModelSelector.retiredValue.spec.tsx \
src/lib/events-manifest/catalog.spec.ts

# Lint gate scoped to the files the PR touched.
eslint:
Expand Down
2 changes: 2 additions & 0 deletions src/components/journey/shared/EventSelector/EventSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
MessageCircle,
MessageSquare,
Megaphone,
ShoppingCart,
Sparkles,
type LucideIcon,
} from 'lucide-react';
Expand Down Expand Up @@ -62,6 +63,7 @@ const CATEGORY_ICON: Record<EventCategory, LucideIcon> = {
conversation: MessageCircle,
message: MessageSquare,
campaign: Megaphone,
purchase: ShoppingCart,
custom: Sparkles,
};

Expand Down
1 change: 1 addition & 0 deletions src/components/segments/SegmentConditionEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const SEGMENT_EVENT_CATEGORY_LABELS: Record<EventCategory, string> = {
conversation: 'Eventos de Conversa',
message: 'Eventos de Mensagem',
campaign: 'Eventos de Campanha',
purchase: 'Eventos de Compra',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue: The segment event picker renders the new purchase category with the hardcoded Portuguese string Eventos de Compra, so English, Spanish, French, and Italian users see a Portuguese category heading even though categories.purchase was added to all locale files.

Triggers: When a non-Portuguese user opens the Performed or LastPerformed event selector.

Suggested fix: Use the events i18n namespace and render t('categories.purchase') (and the existing category labels) instead of hardcoded Portuguese strings.

custom: 'Personalizado',
};

Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/en/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"conversation": "Conversation events",
"message": "Message events",
"campaign": "Campaign events",
"purchase": "Purchase events",
"custom": "Custom"
},
"propertiesForm": {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/es/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"conversation": "Eventos de Conversación",
"message": "Eventos de Mensaje",
"campaign": "Eventos de Campaña",
"purchase": "Eventos de compra",
"custom": "Personalizado"
},
"propertiesForm": {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/fr/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"conversation": "Événements de conversation",
"message": "Événements de message",
"campaign": "Événements de campagne",
"purchase": "Événements d'achat",
"custom": "Personnalisé"
},
"propertiesForm": {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/it/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"conversation": "Eventi di conversazione",
"message": "Eventi di messaggio",
"campaign": "Eventi di campagna",
"purchase": "Eventi di acquisto",
"custom": "Personalizzato"
},
"propertiesForm": {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/pt-BR/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"conversation": "Eventos de Conversa",
"message": "Eventos de Mensagem",
"campaign": "Eventos de Campanha",
"purchase": "Eventos de Compra",
"custom": "Personalizado"
},
"propertiesForm": {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/pt/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"conversation": "Eventos de Conversa",
"message": "Eventos de Mensagem",
"campaign": "Eventos de Campanha",
"purchase": "Eventos de Compra",
"custom": "Personalizado"
},
"propertiesForm": {
Expand Down
13 changes: 9 additions & 4 deletions src/lib/events-manifest/catalog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,29 @@ describe('frontend events manifest mirror', () => {
);
});

it('groups events by category covering all 5 categories', () => {
it('groups events by category covering all 6 categories', () => {
const grouped = Object.fromEntries(EVENT_CATEGORIES.map((c) => [c, getEventsByCategory(c)]));
expect(grouped.contact.length).toBeGreaterThanOrEqual(6);
// EVO-1263: 2 original (created/resolved) + 5 added (activity, first_reply,
// reply_time, bot_handoff, bot_resolved) to mirror EvoFlow::EVENT_NAMES.
expect(grouped.conversation).toHaveLength(7);
expect(grouped.message.length).toBeGreaterThanOrEqual(4);
expect(grouped.campaign.length).toBeGreaterThanOrEqual(4);
// CRM-316: the purchase captured by the CRM webhook, in its own group.
expect(grouped.purchase.map((e) => e.eventName)).toEqual(['purchase.approved']);
expect(grouped.custom).toHaveLength(1);
});

// EVO-1263 (AC1): the manifest is a strict replica of the backend SSOT
// EvoFlow::EVENT_NAMES (lib/events/evo_flow_event_names.rb), which has 22
// canonical names including `custom`. This count is the single guard keeping
// the frontend manifest faithful to the backend enum — keep it strict.
it('mirrors the backend EvoFlow::EVENT_NAMES count exactly (22)', () => {
expect(EVENT_NAMES).toHaveLength(22);
expect(getEventCatalog()).toHaveLength(22);
// 22 + purchase.approved (CRM-316). pipeline.stage_changed exists on the
// backend but is deliberately absent here: the journey builder exposes it
// as its own trigger type, not as a pickable event.
it('mirrors the backend EvoFlow::EVENT_NAMES count exactly (23)', () => {
expect(EVENT_NAMES).toHaveLength(23);
expect(getEventCatalog()).toHaveLength(23);
});

// EVO-1263 (AC1): the 5 conversation events that previously existed only in
Expand Down
33 changes: 33 additions & 0 deletions src/lib/events-manifest/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,13 +308,46 @@ const ENTRIES: EventCatalogEntry[] = [
description: 'User-defined event with free-form key/value properties.',
schema: { required: {}, optional: {} },
},
// CRM-316: an approved purchase captured by the purchase webhook, emitted by
// the CRM with the contact resolved. First-class so a journey trigger can
// filter on product/amount and a segment can ask "bought X" / "spent > Y".
{
eventName: 'purchase.approved',
category: 'purchase',
dtoType: 'track',
labelPt: 'Compra aprovada',
labelEn: 'Purchase approved',
description: 'A purchase was approved on a payment platform and captured as a lead in the CRM.',
schema: {
required: {
provider: f('string', 'Payment platform key (virtu, hotmart, kiwify, cakto)'),
purchase_id: f('string', 'Purchase/order id on the platform'),
pipeline_id: f('uuid'),
pipeline_item_id: f('uuid', 'Card that holds the purchase'),
source: f('string'),
},
optional: {
product: f('string'),
amount: f('number', 'Currency major unit (e.g. 197.5 reais), never cents'),
currency: f('string'),
platform_event: f('string', 'Event name as the platform sent it'),
outcome: f('string', 'created | already_in_pipeline'),
new_contact: f('boolean', 'Whether the purchase created the contact'),
contact_id: f('uuid'),
pipeline_name: f('string'),
pipeline_stage_id: f('uuid'),
pipeline_stage_name: f('string'),
},
},
},
];

export const EVENT_CATEGORIES: readonly EventCategory[] = [
'contact',
'conversation',
'message',
'campaign',
'purchase',
'custom',
] as const;

Expand Down
1 change: 1 addition & 0 deletions src/lib/events-manifest/event-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const EVENT_NAMES = [
'campaign.message.sent',
'campaign.message.opened',
'campaign.message.clicked',
'purchase.approved',
'custom',
] as const;

Expand Down
2 changes: 1 addition & 1 deletion src/lib/events-manifest/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface EventSchema {
optional: Record<string, FieldSpec>;
}

export type EventCategory = 'contact' | 'conversation' | 'message' | 'campaign' | 'custom';
export type EventCategory = 'contact' | 'conversation' | 'message' | 'campaign' | 'purchase' | 'custom';

export type EventDtoType = 'track' | 'identify';

Expand Down
Loading