fix(ct): повторное «Сохранить» на шаге 3 не создаёт доп. строки и поставщиков второй раз
Доп. строки («Логистика», «Доп. расходы») и новые поставщики шага 3 создаются в SP в начале сохранения, а их SP-id попадали в стейт только в самом конце. Если дальше падало сохранение офферов, id терялись, и повторное «Сохранить» создавало те же строки ещё раз. Теперь id запоминаются сразу после создания — и при успехе, и при сбое (applyCreatedIds). Заодно в репозиторий попадают тесты расчёта сумм matrixHelpers.test.ts.
This commit is contained in:
+102
@@ -0,0 +1,102 @@
|
||||
import { applyCreatedIds, buildOfferKey, computeOffer, getSubCols, round2Str } from './matrixHelpers';
|
||||
import { ExtraPosition } from './matrixTypes';
|
||||
|
||||
describe('computeOffer', () => {
|
||||
it('считает сумму в тенге для КП в тенге', () => {
|
||||
const offer = computeOffer(
|
||||
{ supplierId: 's1', positionId: 'p1', itemIndex: 0, pricePerUnit: 150, quantity: 4 },
|
||||
'KZT',
|
||||
1,
|
||||
);
|
||||
|
||||
expect(offer).toMatchObject({
|
||||
supplierId: 's1',
|
||||
positionId: 'p1',
|
||||
currencyCode: 'KZT',
|
||||
pricePerUnit: 150,
|
||||
quantity: 4,
|
||||
pricePerUnitKzt: 150,
|
||||
totalPriceKzt: 600,
|
||||
totalWithExtraKzt: 600,
|
||||
});
|
||||
});
|
||||
|
||||
it('пересчитывает валютную цену по курсу', () => {
|
||||
const offer = computeOffer(
|
||||
{ supplierId: 's1', positionId: 'p1', itemIndex: 0, pricePerUnit: 10, quantity: 3 },
|
||||
'USD',
|
||||
500,
|
||||
);
|
||||
|
||||
expect(offer.pricePerUnitKzt).toBe(5000);
|
||||
expect(offer.totalPriceKzt).toBe(15000);
|
||||
});
|
||||
|
||||
it('ручная цена в тенге важнее пересчёта по курсу', () => {
|
||||
const offer = computeOffer(
|
||||
{ supplierId: 's1', positionId: 'p1', itemIndex: 0, pricePerUnit: 10, priceKztOverride: 4800, quantity: 2 },
|
||||
'USD',
|
||||
500,
|
||||
);
|
||||
|
||||
expect(offer.pricePerUnitKzt).toBe(4800);
|
||||
expect(offer.totalPriceKzt).toBe(9600);
|
||||
});
|
||||
|
||||
it('пустая ячейка даёт нули, а не NaN', () => {
|
||||
const offer = computeOffer({ supplierId: 's1', positionId: 'p1', itemIndex: 0 }, 'KZT', 1);
|
||||
|
||||
expect(offer.pricePerUnit).toBe(0);
|
||||
expect(offer.quantity).toBe(0);
|
||||
expect(offer.totalPriceKzt).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildOfferKey', () => {
|
||||
it('склеивает поставщика и позицию через ::', () => {
|
||||
expect(buildOfferKey('s1', 'p1')).toBe('s1::p1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSubCols', () => {
|
||||
it('для тенге 5 колонок, для валюты 7, в урезанном режиме 3', () => {
|
||||
expect(getSubCols('KZT')).toBe(5);
|
||||
expect(getSubCols('USD')).toBe(7);
|
||||
expect(getSubCols('USD', true)).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('round2Str', () => {
|
||||
it('отрезает хвост плавающей точки', () => {
|
||||
expect(round2Str(275620.94999)).toBe('275620.95');
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyCreatedIds', () => {
|
||||
// Доп. строку создали в SP, а следующий шаг сохранения упал: id должен остаться в стейте,
|
||||
// иначе повторное «Сохранить» создаст строку «Логистика» ещё раз.
|
||||
it('проставляет созданный SP Id', () => {
|
||||
const extras: ExtraPosition[] = [{ id: 'extra_pos_1', materialName: 'Логистика', unit: 'рейс' }];
|
||||
|
||||
expect(applyCreatedIds(extras, { extra_pos_1: { spId: 1601 } })).toEqual([
|
||||
{ id: 'extra_pos_1', materialName: 'Логистика', unit: 'рейс', spId: 1601 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('не трогает строки, которых нет среди созданных', () => {
|
||||
const extras: Array<{ id: string; spId?: number }> = [{ id: 'extra_pos_1', spId: 1500 }, { id: 'extra_pos_2' }];
|
||||
|
||||
expect(applyCreatedIds(extras, { extra_pos_2: { spId: 1601 } })).toEqual([
|
||||
{ id: 'extra_pos_1', spId: 1500 },
|
||||
{ id: 'extra_pos_2', spId: 1601 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('не мутирует исходный массив', () => {
|
||||
const extras = [{ id: 'extra_sup_1' as string, spId: undefined as number | undefined }];
|
||||
|
||||
applyCreatedIds(extras, { extra_sup_1: { spId: 7 } });
|
||||
|
||||
expect(extras[0].spId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -73,3 +73,13 @@ export function computeOffer(draft: CellEdit, currencyCode: string, rate: number
|
||||
export function buildOfferKey(supplierId: string, positionId: string): string {
|
||||
return `${supplierId}::${positionId}`;
|
||||
}
|
||||
|
||||
// Проставляет id, полученные при создании в SP (доп. строки / поставщики Шага 3), в элементы стейта.
|
||||
// Результат сохраняем сразу после создания: если дальше сохранение упадёт, повторное «Сохранить»
|
||||
// увидит spId и не создаст ту же строку ещё раз.
|
||||
export function applyCreatedIds<T extends { id: string }>(
|
||||
items: T[],
|
||||
createdById: Record<string, Partial<T>>,
|
||||
): T[] {
|
||||
return items.map(item => (createdById[item.id] ? { ...item, ...createdById[item.id] } : item));
|
||||
}
|
||||
|
||||
+26
-2
@@ -4,7 +4,7 @@ import { SupplierOffer, CurrencyRate, FormStep } from '../types/domain';
|
||||
import { CtService } from '../../services/CtService';
|
||||
import { CTFormData } from '../hooks/useCtForm';
|
||||
import { ExtraSupplier, ExtraPosition, PositionRow, CellEdit, EditableOffers, CriteriaEntry } from './matrixTypes';
|
||||
import { buildOfferKey, computeOffer } from './matrixHelpers';
|
||||
import { applyCreatedIds, buildOfferKey, computeOffer } from './matrixHelpers';
|
||||
import { IVendorRecord } from '../../services/VendorDatabaseService';
|
||||
|
||||
let extraIdCounter = 0;
|
||||
@@ -670,6 +670,26 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
|
||||
onSave({ step3ExtraPositions: updated });
|
||||
};
|
||||
|
||||
// Созданные на «Сохранить» поставщиков и доп. строки запоминаем сразу — в стейт и сессию. Раньше их
|
||||
// spId попадали в стейт только в самом конце handleSave: упади saveOffers — повторное «Сохранить»
|
||||
// создало бы в SP ещё одну строку «Логистика» / ещё одного поставщика.
|
||||
const rememberCreatedExtras = (
|
||||
supSpIds: Record<string, number>,
|
||||
offerIds: Record<string, number>,
|
||||
posSpIds: Record<string, number>,
|
||||
): void => {
|
||||
const supplierPatches: Record<string, Partial<ExtraSupplier>> = {};
|
||||
Object.keys(supSpIds).forEach(id => { supplierPatches[id] = { spId: supSpIds[id], offerId: offerIds[id] }; });
|
||||
const positionPatches: Record<string, Partial<ExtraPosition>> = {};
|
||||
Object.keys(posSpIds).forEach(id => { positionPatches[id] = { spId: posSpIds[id] }; });
|
||||
|
||||
const nextSuppliers = applyCreatedIds(extraSuppliers, supplierPatches);
|
||||
const nextPositions = applyCreatedIds(extraPositions, positionPatches);
|
||||
setExtraSuppliers(nextSuppliers);
|
||||
setExtraPositions(nextPositions);
|
||||
onSave({ step3ExtraSuppliers: nextSuppliers, step3ExtraPositions: nextPositions });
|
||||
};
|
||||
|
||||
const handleSave = async (): Promise<void> => {
|
||||
if (!ct) return;
|
||||
const deliveryMissingKm = extraPositions.filter(ep =>
|
||||
@@ -685,6 +705,8 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
|
||||
try {
|
||||
const extraSupSpIds: Record<string, number> = {};
|
||||
const extraOfferIds: Record<string, number> = {};
|
||||
const extraPosSpIds: Record<string, number> = {};
|
||||
try {
|
||||
for (const es of extraSuppliers) {
|
||||
if (es.spId) {
|
||||
// Already created in SP — either on a previous save, or eagerly by
|
||||
@@ -698,7 +720,6 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
|
||||
extraOfferIds[es.id] = offerId;
|
||||
}
|
||||
|
||||
const extraPosSpIds: Record<string, number> = {};
|
||||
for (const ep of extraPositions) {
|
||||
if (ep.spId) {
|
||||
// Already created in SP on a previous save — reuse to avoid duplicate Позиции СТ rows.
|
||||
@@ -721,6 +742,9 @@ export function useStep3Matrix(args: UseStep3MatrixArgs) {
|
||||
}]);
|
||||
if (saved[0]?.spId) extraPosSpIds[ep.id] = saved[0].spId;
|
||||
}
|
||||
} finally {
|
||||
rememberCreatedExtras(extraSupSpIds, extraOfferIds, extraPosSpIds);
|
||||
}
|
||||
|
||||
const offersToSave = computedOffers
|
||||
.filter(o => o.totalWithExtraKzt > 0)
|
||||
|
||||
Reference in New Issue
Block a user