Skip to content

Commit 4828267

Browse files
committed
Fixing missed ride organized if empty/self-refference in AI preview
1 parent 07fb1ef commit 4828267

4 files changed

Lines changed: 103 additions & 20 deletions

File tree

‎src/__tests__/commands/ai-ride-command-handler.test.js‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ describe.each(['en', 'ru'])('AiRideCommandHandler (%s)', (language) => {
3838
mockRideService = {
3939
getRide: jest.fn(),
4040
createRideFromParams: jest.fn(),
41-
updateRideFromParams: jest.fn()
41+
updateRideFromParams: jest.fn(),
42+
resolveCreateOrganizer: jest.fn((organizer, profile) => {
43+
if (organizer === 'Bob') return organizer;
44+
return `${profile.firstName} ${profile.lastName} (@${profile.username})`;
45+
})
4246
};
4347

4448
mockMessageFormatter = {
@@ -607,6 +611,47 @@ describe.each(['en', 'ru'])('AiRideCommandHandler (%s)', (language) => {
607611
});
608612
});
609613

614+
describe('preview organizer', () => {
615+
const setupAndProcess = async (params) => {
616+
mockAiRideService.parseRideText.mockResolvedValue({ params, error: null });
617+
mockCtx.message = { text: 'some text', message_id: 5 };
618+
handler.states.set('42:100', {
619+
mode: 'create', rideId: null, ride: null,
620+
userMessages: [], messageCount: 0, lastParams: null,
621+
previewMessageId: null, botMessageIds: [],
622+
routeInfoCache: {}
623+
});
624+
625+
await handler.handleTextInput(mockCtx);
626+
627+
return mockMessageFormatter.formatRidePreview.mock.calls[0]?.[0];
628+
};
629+
630+
it('shows the creator as organizer in create preview when organizer is empty', async () => {
631+
const preview = await setupAndProcess({ title: 'Ride', when: 'tomorrow' });
632+
633+
expect(preview.organizer).toBe('Alice Smith (@alice)');
634+
expect(mockRideService.resolveCreateOrganizer).toHaveBeenCalledWith(
635+
undefined,
636+
expect.objectContaining({ userId: 42, username: 'alice', firstName: 'Alice', lastName: 'Smith' }),
637+
{ language }
638+
);
639+
});
640+
641+
it('shows the creator as organizer in create preview when organizer refers to self', async () => {
642+
const selfOrganizer = language === 'ru' ? 'я' : 'me';
643+
const preview = await setupAndProcess({ title: 'Ride', when: 'tomorrow', organizer: selfOrganizer });
644+
645+
expect(preview.organizer).toBe('Alice Smith (@alice)');
646+
});
647+
648+
it('keeps explicit organizer in create preview when organizer is someone else', async () => {
649+
const preview = await setupAndProcess({ title: 'Ride', when: 'tomorrow', organizer: 'Bob' });
650+
651+
expect(preview.organizer).toBe('Bob');
652+
});
653+
});
654+
610655
// ─── route info enrichment in preview ───────────────────────────────────────
611656

612657
describe('route info enrichment in preview', () => {

‎src/__tests__/services/ride-service.test.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,19 @@ describe('RideService', () => {
717717
expect(result.ride.organizer).toBe('me');
718718
});
719719

720+
it('should resolve create organizer consistently for preview and persistence', () => {
721+
const user = new UserProfile({
722+
userId: 789,
723+
firstName: 'Test',
724+
lastName: 'User',
725+
username: 'testuser'
726+
});
727+
728+
expect(rideService.resolveCreateOrganizer(undefined, user, { language: 'ru' })).toBe('Test User (@testuser)');
729+
expect(rideService.resolveCreateOrganizer('я', user, { language: 'ru' })).toBe('Test User (@testuser)');
730+
expect(rideService.resolveCreateOrganizer('Bob', user, { language: 'ru' })).toBe('Bob');
731+
});
732+
720733
it('should handle organizer field when updating a ride', async () => {
721734
// First create a ride
722735
const ride = await rideService.createRide(testRide);

‎src/commands/AiRideCommandHandler.js‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,11 @@ export class AiRideCommandHandler extends BaseCommandHandler {
231231
// state.lastParams stays as pure AI output; RideService re-parses on confirm.
232232
const previewParams = await this._enrichWithRouteInfo(params, state);
233233

234-
const previewObj = this._buildPreviewObject(previewParams, state);
234+
const creatorProfile = UserProfile.fromTelegramUser(ctx.from);
235+
const previewObj = this._buildPreviewObject(previewParams, state, {
236+
creatorProfile,
237+
language: ctx.lang
238+
});
235239
const previewText = this.messageFormatter.formatRidePreview(previewObj, ctx.lang);
236240
const atLimit = state.messageCount >= MAX_DIALOG_MESSAGES;
237241
const fullText = atLimit
@@ -338,8 +342,9 @@ export class AiRideCommandHandler extends BaseCommandHandler {
338342
* reflects the full final state (existing + changes).
339343
* @param {Object} params - AI-extracted params
340344
* @param {Object|null} state - handler state (used to access existing ride in update mode)
345+
* @param {{creatorProfile?: UserProfile|null, language?: string}} options
341346
*/
342-
_buildPreviewObject(params, state = null) {
347+
_buildPreviewObject(params, state = null, options = {}) {
343348
const existingRide = state?.mode === 'update' ? state.ride : null;
344349
const routeInputs = params.routes ?? (params.route ? [params.route] : null);
345350
const clearsRoutes = Array.isArray(routeInputs) && routeInputs.length === 1 && routeInputs[0] === '-';
@@ -351,7 +356,7 @@ export class AiRideCommandHandler extends BaseCommandHandler {
351356
title: params.title || existingRide?.title || null,
352357
date: null,
353358
category: null,
354-
organizer: params.organizer || existingRide?.organizer || null,
359+
organizer: this._resolvePreviewOrganizer(params, state, options),
355360
meetingPoint: params.meet || existingRide?.meetingPoint || null,
356361
routes: clearsRoutes ? [] : (routeInputs ? (parsedPreviewRoutes || null) : getRideRoutes(existingRide)),
357362
distance: params.dist ? parseFloat(params.dist)
@@ -407,6 +412,26 @@ export class AiRideCommandHandler extends BaseCommandHandler {
407412
return preview;
408413
}
409414

415+
/**
416+
* Resolve organizer for preview using the same creator fallback as ride creation.
417+
* @param {Object} params
418+
* @param {Object|null} state
419+
* @param {{creatorProfile?: UserProfile|null, language?: string}} options
420+
* @returns {string|null}
421+
*/
422+
_resolvePreviewOrganizer(params, state = null, options = {}) {
423+
const existingRide = state?.mode === 'update' ? state.ride : null;
424+
if (existingRide) {
425+
return params.organizer || existingRide.organizer || null;
426+
}
427+
428+
return this.rideService.resolveCreateOrganizer(
429+
params.organizer,
430+
options.creatorProfile || null,
431+
{ language: options.language }
432+
) || null;
433+
}
434+
410435
async _executeRideOperation(ctx, stateKey, state) {
411436
const options = { language: ctx.lang };
412437
const creatorProfile = UserProfile.fromTelegramUser(ctx.from);

‎src/services/RideService.js‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,7 @@ export class RideService {
206206
};
207207

208208
// Set organizer name - use provided value or default to creator's name
209-
if (this.isSelfOrganizerReference(rideData.organizer, language)) {
210-
rideData.organizer = '';
211-
}
212-
if (!rideData.organizer && creatorProfile) {
213-
rideData.organizer = this.getDefaultOrganizer(creatorProfile);
214-
}
209+
rideData.organizer = this.resolveCreateOrganizer(rideData.organizer, creatorProfile, { language });
215210

216211
const ride = await this.createRide(rideData, creatorProfile);
217212
return { ride, error: null };
@@ -244,27 +239,32 @@ export class RideService {
244239
}
245240

246241
/**
247-
* Detect organizer values that mean "the creator".
242+
* Resolve organizer value for a newly created ride.
248243
* @param {string} organizer
249-
* @param {string} language
250-
* @returns {boolean}
244+
* @param {UserProfile|null} creatorProfile
245+
* @param {{language?: string}} options
246+
* @returns {string}
251247
*/
252-
isSelfOrganizerReference(organizer, language) {
253-
if (typeof organizer !== 'string') return false;
254-
248+
resolveCreateOrganizer(organizer, creatorProfile, options = {}) {
255249
const references = t(
256-
language || config.i18n.defaultLanguage,
250+
options.language || config.i18n.defaultLanguage,
257251
'services.ride.selfOrganizerReferences',
258252
{},
259253
{
260254
fallbackLanguage: config.i18n.fallbackLanguage,
261255
withMissingMarker: false
262256
}
263257
);
264-
if (!Array.isArray(references)) return false;
258+
const normalized = typeof organizer === 'string'
259+
? organizer.trim().toLowerCase().replace(/[.!?]+$/g, '')
260+
: '';
261+
const refersToCreator = Array.isArray(references) && references.includes(normalized);
262+
263+
if (!organizer || refersToCreator) {
264+
return creatorProfile ? this.getDefaultOrganizer(creatorProfile) : '';
265+
}
265266

266-
const normalized = organizer.trim().toLowerCase().replace(/[.!?]+$/g, '');
267-
return references.includes(normalized);
267+
return organizer;
268268
}
269269

270270
/**

0 commit comments

Comments
 (0)