Merge pull request 'Эпик 2 ревью: race-guard в геокодинге ПВЗ + фикс flaky теста' (#2) from chore/review-E2-pvz-fixes into marketplace2

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-05-20 11:59:54 +00:00
4 changed files with 29 additions and 6 deletions
@@ -101,17 +101,24 @@ export class KuDetailsService {
try {
const result = await this.geocoder.geocode(addressFull);
if (result.status === 'OK') {
await this.repo.updateGeocode(coopname, coreBraname, {
const saved = await this.repo.updateGeocode(coopname, coreBraname, {
status: 'OK',
lat: result.lat,
lng: result.lng,
geocodedAt: new Date(),
expectedAddressFull: addressFull,
});
if (!saved) {
this.logger.warn(
`Геокодинг (${coopname}, ${coreBraname}) завершён, но адрес был изменён до записи — координаты не сохранены`
);
}
} else {
await this.repo.updateGeocode(coopname, coreBraname, {
status: 'FAILED',
errorMessage: result.errorMessage,
geocodedAt: new Date(),
expectedAddressFull: addressFull,
});
}
} catch (err: unknown) {
@@ -121,6 +128,7 @@ export class KuDetailsService {
status: 'FAILED',
errorMessage: message,
geocodedAt: new Date(),
expectedAddressFull: addressFull,
});
}
}
@@ -12,6 +12,12 @@ export interface KuDetailsDomainRepository {
save(entity: KuDetailsDomainEntity): Promise<KuDetailsDomainEntity>;
/**
* Записывает результат геокодинга. `expectedAddressFull` — guard от race
* condition: если адрес КУ был изменён пока геокодинг работал, результат
* не сохраняется (репозиторий возвращает null), и UI не получает
* координаты несуществующего адреса.
*/
updateGeocode(
coopname: string,
coreBraname: string,
@@ -21,6 +27,7 @@ export interface KuDetailsDomainRepository {
lng?: number;
errorMessage?: string;
geocodedAt: Date;
expectedAddressFull?: string;
}
): Promise<KuDetailsDomainEntity | null>;
@@ -62,11 +62,16 @@ export class KuDetailsRepositoryAdapter implements KuDetailsDomainRepository {
lng?: number;
errorMessage?: string;
geocodedAt: Date;
expectedAddressFull?: string;
}
): Promise<KuDetailsDomainEntity | null> {
const existing = await this.repo.findOne({ where: { coopname, coreBraname } });
if (!existing) return null;
if (payload.expectedAddressFull !== undefined && existing.addressFull !== payload.expectedAddressFull) {
return null;
}
existing.geocodeStatus = payload.status;
existing.lat = payload.lat;
existing.lng = payload.lng;
@@ -1,5 +1,11 @@
// Unit-тесты фабрики geocoderPortFactory: выбор реализации по
// config.geocoder.provider. Noop-провайдер всегда отвечает FAILED.
//
// Тест использует jest.resetModules + динамический import для подмены
// config.geocoder.provider между кейсами. Из-за этого статический
// import класса adapter'а и динамический import factory отдают разные
// инстансы конструктора — `instanceof` сравнение не работает. Поэтому
// проверяем имя конструктора через `constructor.name`.
jest.mock('~/config/config', () => ({
__esModule: true,
@@ -14,9 +20,6 @@ jest.mock('~/config/config', () => ({
},
}));
import { NoopGeocoderAdapter } from '~/extensions/marketplace/infrastructure/adapters/noop-geocoder.adapter';
import { YandexGeocoderAdapter } from '~/extensions/marketplace/infrastructure/adapters/yandex-geocoder.adapter';
describe('geocoderPortFactory', () => {
afterEach(() => {
jest.resetModules();
@@ -27,7 +30,7 @@ describe('geocoderPortFactory', () => {
'~/extensions/marketplace/infrastructure/adapters/geocoder.factory'
);
const port = geocoderPortFactory();
expect(port).toBeInstanceOf(NoopGeocoderAdapter);
expect(port.constructor.name).toBe('NoopGeocoderAdapter');
const r = await port.geocode('addr');
expect(r.status).toBe('FAILED');
if (r.status === 'FAILED') expect(r.errorMessage).toMatch(/GEOCODER_PROVIDER=noop/);
@@ -51,6 +54,6 @@ describe('geocoderPortFactory', () => {
'~/extensions/marketplace/infrastructure/adapters/geocoder.factory'
);
const port = geocoderPortFactory();
expect(port).toBeInstanceOf(YandexGeocoderAdapter);
expect(port.constructor.name).toBe('YandexGeocoderAdapter');
});
});