fix(ct): повторная обработка КП не оставляет в SP дубли предложений

Каждая обработка КП создаёт записи в «Оценка поставщиков», а прежние удалялись только
по «Сохранить». Исправленный КП поставщика или файл, загруженный заново, оставлял рядом
старые записи — отсюда дубли в SP (СТ №89, №4). Теперь после создания записи прежние
записи того же поставщика по той же позиции уходят в корзину; порядок «создать, потом
удалить» — чтобы при сбое не потерять предложение.
This commit is contained in:
Тимур Городецкий
2026-09-30 16:12:46 +05:00
parent 76ad9c6242
commit 2316f8bd6d
3 changed files with 99 additions and 2 deletions
@@ -24,7 +24,11 @@ import {
getRequestTypeIdByPageUrl,
writeRoute,
} from "../../../shared/approval";
import { keepLatestOfferRecords, parseOfferItemIndex } from "../../../shared/offers/offerRecords";
import {
findSupersededOfferIds,
keepLatestOfferRecords,
parseOfferItemIndex,
} from "../../../shared/offers/offerRecords";
// Round monetary values to 2 decimals before persisting to SharePoint (drops float tails).
function round2(value: number): number {
@@ -1598,6 +1602,46 @@ export class CtService {
);
if (!res.ok)
throw new Error(`Ошибка сохранения предложения: ${res.status}`);
// Повторная обработка КП (исправленный КП, файл загружен заново) раньше добавляла записи рядом
// с прежними — в SP копились дубли, если потом не нажимали «Сохранить». Сначала создаём новую
// запись, потом убираем прежние: упади создание — старое предложение останется.
const created = (await res.json()) as { Id: number };
await this._recycleSupersededOffers({
id: created.Id,
supplierSpId: params.supplierSpId,
positionSpId: params.positionSpId,
});
}
private async _recycleSupersededOffers(saved: {
id: number;
supplierSpId: number;
positionSpId: number;
}): Promise<void> {
const url =
`${this._li(LISTS.OFFERS)}?$select=Id,Title,positionId,supplierId` +
`&$filter=supplierId eq ${saved.supplierSpId} and positionId eq ${saved.positionSpId}`;
const res = await this.http.get(url, SPHttpClient.configurations.v1);
// Не критично для обработки КП: оставшиеся дубли отсеивают загрузчики (keepLatestOfferRecords),
// а «Сохранить» перезапишет записи позиции целиком. Но молча не глотаем.
if (!res.ok) {
console.warn(`[CtService] не удалось прочитать прежние предложения: ${res.status}`);
return;
}
const data = await res.json();
const existing = ((data.value ?? []) as Array<Record<string, unknown>>).map((item) => ({
id: Number(item.Id),
supplierSpId: Number(item.supplierId),
positionSpId: Number(item.positionId),
itemIndex: parseOfferItemIndex(String(item.Title ?? "")),
}));
// MERGE на этом списке блокирует ресивер — убираем через recycle(), как в saveOffers.
await this._runChunked(findSupersededOfferIds(existing, saved), 20, async (id) => {
await this.http
.post(`${this._liById(LISTS.OFFERS, id)}/recycle()`, SPHttpClient.configurations.v1, {})
.catch(() => undefined /* уже в корзине / не найден — игнорируем */);
});
}
// ─── Mapping ──────────────────────────────────────────────────────────────
@@ -1,4 +1,4 @@
import { IOfferRecordKey, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords';
import { IOfferRecordKey, findSupersededOfferIds, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords';
interface ITestRecord extends IOfferRecordKey {
price: number;
@@ -57,3 +57,40 @@ describe('keepLatestOfferRecords', () => {
expect(keepLatestOfferRecords(records)).toHaveLength(2);
});
});
describe('findSupersededOfferIds', () => {
// Повторная обработка КП поставщика 259: новая запись 1850 заменяет прежнее предложение по позиции.
const saved = makeRecord({ id: 1850, positionSpId: 1478 });
it('возвращает прежние записи того же поставщика по той же позиции', () => {
const existing = [makeRecord({ id: 1836, positionSpId: 1478 }), saved];
expect(findSupersededOfferIds(existing, saved)).toEqual([1836]);
});
it('прежние строки комплекта тоже заменяются новым КП', () => {
const existing = [
makeRecord({ id: 1836, positionSpId: 1478, itemIndex: 0 }),
makeRecord({ id: 1837, positionSpId: 1478, itemIndex: 1 }),
saved,
];
expect(findSupersededOfferIds(existing, saved)).toEqual([1836, 1837]);
});
it('не трогает другие позиции и других поставщиков', () => {
const existing = [
makeRecord({ id: 1836, positionSpId: 1476 }),
makeRecord({ id: 1837, positionSpId: 1478, supplierSpId: 233 }),
saved,
];
expect(findSupersededOfferIds(existing, saved)).toEqual([]);
});
it('не трогает запись новее сохранённой (параллельная обработка)', () => {
const existing = [saved, makeRecord({ id: 1900, positionSpId: 1478 })];
expect(findSupersededOfferIds(existing, saved)).toEqual([]);
});
});
@@ -30,3 +30,19 @@ export function keepLatestOfferRecords<T extends IOfferRecordKey>(records: T[]):
}
return Array.from(latestByKey.values());
}
// Какие записи вытеснила только что сохранённая: прежние записи того же поставщика по той же позиции,
// включая строки комплекта — новый КП поставщика заменяет его прежнее предложение целиком.
// Записи новее сохранённой не трогаем: их могла создать параллельная обработка.
export function findSupersededOfferIds(
existing: IOfferRecordKey[],
saved: Pick<IOfferRecordKey, 'id' | 'supplierSpId' | 'positionSpId'>,
): number[] {
return existing
.filter(record =>
record.supplierSpId === saved.supplierSpId &&
record.positionSpId === saved.positionSpId &&
record.id < saved.id
)
.map(record => record.id);
}