fix(ct): повторная обработка КП не оставляет в SP дубли предложений
Каждая обработка КП создаёт записи в «Оценка поставщиков», а прежние удалялись только по «Сохранить». Исправленный КП поставщика или файл, загруженный заново, оставлял рядом старые записи — отсюда дубли в SP (СТ №89, №4). Теперь после создания записи прежние записи того же поставщика по той же позиции уходят в корзину; порядок «создать, потом удалить» — чтобы при сбое не потерять предложение.
This commit is contained in:
@@ -24,7 +24,11 @@ import {
|
|||||||
getRequestTypeIdByPageUrl,
|
getRequestTypeIdByPageUrl,
|
||||||
writeRoute,
|
writeRoute,
|
||||||
} from "../../../shared/approval";
|
} 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).
|
// Round monetary values to 2 decimals before persisting to SharePoint (drops float tails).
|
||||||
function round2(value: number): number {
|
function round2(value: number): number {
|
||||||
@@ -1598,6 +1602,46 @@ export class CtService {
|
|||||||
);
|
);
|
||||||
if (!res.ok)
|
if (!res.ok)
|
||||||
throw new Error(`Ошибка сохранения предложения: ${res.status}`);
|
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 ──────────────────────────────────────────────────────────────
|
// ─── Mapping ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IOfferRecordKey, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords';
|
import { IOfferRecordKey, findSupersededOfferIds, keepLatestOfferRecords, parseOfferItemIndex } from './offerRecords';
|
||||||
|
|
||||||
interface ITestRecord extends IOfferRecordKey {
|
interface ITestRecord extends IOfferRecordKey {
|
||||||
price: number;
|
price: number;
|
||||||
@@ -57,3 +57,40 @@ describe('keepLatestOfferRecords', () => {
|
|||||||
expect(keepLatestOfferRecords(records)).toHaveLength(2);
|
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());
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user