fix rate edit

This commit is contained in:
Marshal
2026-07-17 10:15:26 +00:00
parent 18311f22f7
commit 801872c106
20 changed files with 1227 additions and 112 deletions

View File

@@ -119,12 +119,62 @@ export class RatesService {
});
}
/** Update a DRAFT rate. */
/**
* Update a DRAFT rate in place. Nothing prices off a draft, so a direct edit
* is safe. A LIVE rate cannot take this path — see `applyApprovedUpdate`.
*/
async update(id: string, dto: UpdateRateDto): Promise<Rate> {
const existing = await this.findById(id);
if (existing.status !== 'DRAFT') {
throw new BadRequestException('Only DRAFT rates can be updated');
throw new BadRequestException(
existing.status === 'LIVE'
? 'A LIVE rate cannot be edited directly — file a rate change request so an approver can apply it.'
: 'Only DRAFT rates can be updated',
);
}
return this.applyUpdate(existing, dto);
}
/**
* Apply an approved change request to a LIVE rate. Same validation as a
* DRAFT edit — it just skips the DRAFT guard, because a LIVE rate reaching
* here has already been through approval. Only ever called by
* RateChangeRequestsService.approve.
*/
async applyApprovedUpdate(id: string, dto: UpdateRateDto): Promise<Rate> {
const existing = await this.findById(id);
if (existing.status !== 'LIVE') {
throw new BadRequestException(
`Rate change requests apply to LIVE rates only — this rate is ${existing.status}.`,
);
}
return this.applyUpdate(existing, dto);
}
/**
* Validate a proposed patch against a rate without writing anything — lets a
* change request be refused at submit time instead of surprising the
* approver. Throws exactly what applying it would throw.
*/
async assertUpdateValid(id: string, dto: UpdateRateDto): Promise<void> {
await this.buildUpdate(await this.findById(id), dto);
}
private async applyUpdate(existing: Rate, dto: UpdateRateDto): Promise<Rate> {
const updates = await this.buildUpdate(existing, dto);
const updated = await this.repository.update(existing.id, updates);
if (!updated) throw new NotFoundException(`Rate ${existing.id} not found`);
return updated;
}
/**
* The shared edit body: re-derives rateType, re-validates the unit against
* the (possibly changed) shape, and guards pattern uniqueness. Status is
* never touched — an approved edit to a LIVE rate stays LIVE. Pure apart
* from the uniqueness read, so it doubles as the dry-run validator.
*/
private async buildUpdate(existing: Rate, dto: UpdateRateDto): Promise<Partial<Rate>> {
const id = existing.id;
const updates: Partial<Rate> = {};
const appliesTo = (dto.appliesTo as Rate['appliesTo']) ?? existing.appliesTo;
@@ -179,9 +229,7 @@ export class RatesService {
updates.currency = dto.currency ?? existing.currency ?? 'USD';
if (dto.rateValue !== undefined) updates.rateValue = dto.rateValue;
const updated = await this.repository.update(id, updates);
if (!updated) throw new NotFoundException(`Rate ${id} not found`);
return updated;
return updates;
}
/** Submit a DRAFT rate for CEO approval. */