Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 5x 5x 5x 1x 1x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 4x 4x 2x 2x 2x 2x 2x 2x 2x 2x 2x 4x 1x 1x 2x 2x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 2x 2x 2x 2x 2x 2x 2x 4x 1x 4x 4x 4x 2x 2x 2x 2x 4x 2x 2x 4x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 1x 1x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 1x 1x 1x 1x 1x 3x 3x 4x 1x 1x 1x 1x 1x 1x 1x 4x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 1x 2x 2x 2x 2x 1x 1x 1x 1x 1x 2x 1x 1x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 40x 40x 40x 25x 25x 1x | import { HttpException, HttpStatus, Inject, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { pick } from 'lodash-es';
import { DataSource, Repository } from 'typeorm';
import { Payment, type Company, type Overwrite, type Statement } from '@amalia/core/models';
import { OverwriteTypesEnum, type PaymentContract } from '@amalia/core/types';
import { OverwritesService } from '@amalia/data-correction/overwrites/core';
import { OverwriteScopeEnum } from '@amalia/data-correction/overwrites/types';
import { toError } from '@amalia/ext/typescript';
import { type AuthenticatedContext } from '@amalia/kernel/auth/types';
import { formatPaymentResponse } from '../dto/paymentResponse.dto';
import { PaymentLockService } from '../paymentLock/paymentLock.service';
@Injectable()
export class PaymentsOverwriteService {
private readonly logger = new Logger(PaymentsOverwriteService.name);
public constructor(
private readonly connection: DataSource,
private readonly overwriteService: OverwritesService,
@InjectRepository(Payment)
private readonly paymentRepository: Repository<Payment>,
@Inject(PaymentLockService)
private readonly paymentLockService: PaymentLockService,
) {}
/**
* Update a payment.
* @param company
* @param existingPayment
* @param authenticatedContext
* @param patch
*/
public async update(
company: Company,
existingPayment: Payment,
authenticatedContext: AuthenticatedContext,
patch: Partial<Payment>,
): Promise<PaymentContract> {
const field = 'paymentPeriodId';
await this.checkOverwriteIsDoable(company, patch, field, existingPayment);
const overwriteToClear = await this.overwriteService.findOneByPaymentIdAndField(company, existingPayment.id, field);
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. Clear overwrite if already exists.
if (overwriteToClear) {
await this.overwriteService.clear(company, overwriteToClear, authenticatedContext, queryRunner);
}
// Keep source value of the cleared overwrite if found
const sourceValue = overwriteToClear ? overwriteToClear.sourceValue : existingPayment.paymentPeriodId || null;
// 2. Create new overwrite
const overwrite = await this.overwriteService.create(
company,
authenticatedContext,
{
...patch,
company,
companyId: company.id,
creator: authenticatedContext.user,
statementId: existingPayment.statementId,
field,
paymentId: existingPayment.id,
overwriteValue: patch.paymentPeriodId || null,
sourceValue,
overwriteType: OverwriteTypesEnum.PAYMENT,
createdOnStatementId: existingPayment.statementId,
scope: OverwriteScopeEnum.STATEMENT,
},
queryRunner,
);
// 3. update payment
const paymentToUpdate = {
...existingPayment,
...patch,
// If we overwrite the paymentPeriodId, we want to reset the error on the payment
...('paymentPeriodId' in patch &&
!!patch.paymentPeriodId && {
error: null,
}),
};
const updatedPayment = await this.paymentRepository.save(paymentToUpdate);
await queryRunner.commitTransaction();
return formatPaymentResponse(updatedPayment, overwrite);
} catch (err) {
const error = toError(err);
this.logger.error({ message: 'Error on updating payment', error, company: pick(company, ['id', 'name']) });
await queryRunner.rollbackTransaction();
throw new HttpException('Error on updating payment, please contact admin', HttpStatus.INTERNAL_SERVER_ERROR);
} finally {
await queryRunner.release();
}
}
/**
* Clear payment: clear overwrites on payment, and update payment with previous value.
* @param company
* @param authenticatedContext
* @param payment
* @param overwriteId
* @param statementId
*/
public async clearPaymentOverwrite(
company: Company,
authenticatedContext: AuthenticatedContext,
payment: Payment,
overwriteId: string,
statementId: string,
): Promise<PaymentContract> {
const overwrite = await this.overwriteService.findOne(
company,
overwriteId,
{ id: statementId } as Statement,
undefined,
payment,
);
await this.checkOverwriteIsClearable(company, payment, overwrite);
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. Clear overwrite if already exists.
await this.overwriteService.clear(company, overwrite, authenticatedContext, queryRunner);
const { sourceValue } = overwrite;
// 2. update payment
const paymentToUpdate = {
...payment,
paymentPeriodId: sourceValue as string,
};
const updatedPayment = await queryRunner.manager.getRepository(Payment).save(paymentToUpdate);
await queryRunner.commitTransaction();
return formatPaymentResponse(updatedPayment, overwrite);
} catch (err) {
const error = toError(err);
this.logger.error({ message: 'Error on clearing payment', error, company: pick(company, ['id', 'name']) });
await queryRunner.rollbackTransaction();
throw new HttpException('Error on clearing payment, please contact admin', HttpStatus.INTERNAL_SERVER_ERROR);
} finally {
await queryRunner.release();
}
}
/**
* Check that we're not creating an overwrite from/to a locked payment period.
* @param company
* @param patch
* @param field
* @param existingPayment
* @private
*/
private async checkOverwriteIsDoable(
company: Company,
patch: Partial<Payment>,
field: string,
existingPayment: Payment,
) {
const paymentLocks = await this.paymentLockService.getPaymentLocks(company);
const matchingLockingContext = paymentLocks.find((lc) => lc.period.id === patch.paymentPeriodId);
// Creating overwrite on a locked context.
if (matchingLockingContext) {
throw new HttpException(
`Cannot apply overwrite because of locked period: ${matchingLockingContext.period.name}`,
423,
);
}
// Moving the payment out of a locked context.
if (field === 'paymentPeriodId' && paymentLocks.some((p) => p.period.id === existingPayment.paymentPeriodId)) {
throw new HttpException(
`Cannot edit overwrite because of locked period: ${
paymentLocks.find((lc) => lc.period.id === existingPayment.paymentPeriodId)!.period.name
}`,
423,
);
}
}
/**
* Check that overwrite is not on a locked period, or will not go to another locked period if cleared.
*
* @param company
* @param payment
* @param overwrite
* @private
*/
private async checkOverwriteIsClearable(company: Company, payment: Payment, overwrite: Overwrite) {
const paymentLocks = await this.paymentLockService.getPaymentLocks(company);
const blockingContexts = paymentLocks
// Can't clear an overwrite on a locked period.
.filter(
(lc) =>
lc.period.id === payment.paymentPeriodId ||
// Can't clear an overwrite that will go back on a locked period.
lc.period.id === overwrite.sourceValue,
);
if (overwrite.field === 'paymentPeriodId' && blockingContexts.length > 0) {
throw new HttpException(
`Cannot edit overwrite because of locked period: ${blockingContexts.map((lc) => lc.period.name).join(', ')}`,
423,
);
}
}
public async populatePaymentsWithOverwrites(
company: Company,
payments: Payment[],
relations?: string[],
): Promise<(Payment & { overwrites: Overwrite[] })[]> {
// Extract the ids of the payments we're about to save.
const paymentIds = payments.map((p) => p.id).filter(Boolean);
// Gather the list of overwrites if the payments are already in db.
const overwritesForThosePayments = await this.overwriteService.findByPaymentIds(company, paymentIds, relations);
// Build a hashmap indexed by paymentId containing all overwrites for that payment.
const overwritesMap: Record<string, Overwrite[] | undefined> = Object.groupBy(
overwritesForThosePayments,
(overwrite) => overwrite.paymentId!,
);
// For each payment, attach overwrites.
return payments.map((p) => ({
...p,
// If we have a payment id, gather the overwrites, else return empty array.
overwrites: (p.id && overwritesMap[p.id]) || [],
}));
}
}
|