fix(cards): FK card_responses via relation JoinColumn + save(create)
Double mapping @Column/@JoinColumn sur id_card ; persist response via card/user relations. Garde update() sur l’instance (évite save parent qui nullifiait les FK enfants). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
|||||||
JoinColumn,
|
JoinColumn,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
|
RelationId,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { CardInstance } from './card_instances.entity';
|
import { CardInstance } from './card_instances.entity';
|
||||||
import { Users } from './users.entity';
|
import { Users } from './users.entity';
|
||||||
@@ -20,20 +21,21 @@ export class CardResponse {
|
|||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@Column({ name: 'id_card', type: 'uuid' })
|
/** FK carte — une seule source de vérité via @JoinColumn (pas de @Column doublon). */
|
||||||
id_card: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => CardInstance, (c) => c.responses, { onDelete: 'CASCADE' })
|
@ManyToOne(() => CardInstance, (c) => c.responses, { onDelete: 'CASCADE' })
|
||||||
@JoinColumn({ name: 'id_card', referencedColumnName: 'id' })
|
@JoinColumn({ name: 'id_card', referencedColumnName: 'id' })
|
||||||
card: CardInstance;
|
card: CardInstance;
|
||||||
|
|
||||||
@Column({ name: 'id_utilisateur', type: 'uuid' })
|
@RelationId((r: CardResponse) => r.card)
|
||||||
id_utilisateur: string;
|
id_card: string;
|
||||||
|
|
||||||
@ManyToOne(() => Users, { onDelete: 'CASCADE' })
|
@ManyToOne(() => Users, { onDelete: 'CASCADE' })
|
||||||
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
|
@JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' })
|
||||||
user: Users;
|
user: Users;
|
||||||
|
|
||||||
|
@RelationId((r: CardResponse) => r.user)
|
||||||
|
id_utilisateur: string;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: 'enum',
|
type: 'enum',
|
||||||
enum: CardResponseActionType,
|
enum: CardResponseActionType,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ describe('CardsService (#194)', () => {
|
|||||||
const cardsRepo = {
|
const cardsRepo = {
|
||||||
create: jest.fn((x) => x),
|
create: jest.fn((x) => x),
|
||||||
save: jest.fn(async (x) => ({ ...x, id: x.id ?? 'card-1', cree_le: new Date(), modifie_le: new Date() })),
|
save: jest.fn(async (x) => ({ ...x, id: x.id ?? 'card-1', cree_le: new Date(), modifie_le: new Date() })),
|
||||||
|
update: jest.fn(async () => ({ affected: 1 })),
|
||||||
findOne: jest.fn(),
|
findOne: jest.fn(),
|
||||||
delete: jest.fn(),
|
delete: jest.fn(),
|
||||||
createQueryBuilder: jest.fn(),
|
createQueryBuilder: jest.fn(),
|
||||||
@@ -37,7 +38,8 @@ describe('CardsService (#194)', () => {
|
|||||||
};
|
};
|
||||||
const responsesRepo = {
|
const responsesRepo = {
|
||||||
create: jest.fn((x) => x),
|
create: jest.fn((x) => x),
|
||||||
save: jest.fn(),
|
save: jest.fn(async (x) => x),
|
||||||
|
insert: jest.fn(),
|
||||||
};
|
};
|
||||||
const amChildrenRepo = { findOne: jest.fn() };
|
const amChildrenRepo = { findOne: jest.fn() };
|
||||||
const parentsChildrenRepo = { find: jest.fn(), findOne: jest.fn() };
|
const parentsChildrenRepo = { find: jest.fn(), findOne: jest.fn() };
|
||||||
@@ -226,4 +228,57 @@ describe('CardsService (#194)', () => {
|
|||||||
expect(ids).toEqual(['am-1', 'p-1', 'p-2']);
|
expect(ids).toEqual(['am-1', 'p-1', 'p-2']);
|
||||||
expect(saved.find((m) => m.id_utilisateur === 'p-2')?.is_creator).toBe(false);
|
expect(saved.find((m) => m.id_utilisateur === 'p-2')?.is_creator).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('parent accepte conge_am → response via relation + carte traitee', async () => {
|
||||||
|
cardsRepo.findOne.mockResolvedValue({
|
||||||
|
id: 'card-1',
|
||||||
|
type_code: 'conge_am',
|
||||||
|
id_placement: 'pl-1',
|
||||||
|
id_evenement: 'ev-1',
|
||||||
|
cree_par: 'am-1',
|
||||||
|
operation: CardOperationType.CREATE,
|
||||||
|
statut: CardInstanceStatutType.OUVERTE,
|
||||||
|
payload: { date_debut: '2026-10-27', date_fin: '2026-10-31' },
|
||||||
|
purge_at: new Date(),
|
||||||
|
cree_le: new Date(),
|
||||||
|
modifie_le: new Date(),
|
||||||
|
type: {
|
||||||
|
titre: 'Congé AM',
|
||||||
|
response_mode: CardResponseModeType.ACCEPT_REFUSE,
|
||||||
|
retention_days: 14,
|
||||||
|
},
|
||||||
|
responses: [],
|
||||||
|
audience: [
|
||||||
|
{ id_utilisateur: 'am-1' },
|
||||||
|
{ id_utilisateur: 'p-1' },
|
||||||
|
{ id_utilisateur: 'p-2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
audienceRepo.findOne.mockResolvedValue({ id_card: 'card-1', id_utilisateur: 'p-1' });
|
||||||
|
evenementsService.maj.mockResolvedValue({});
|
||||||
|
|
||||||
|
const res = await service.repondre('p-1', RoleType.PARENT, 'card-1', {
|
||||||
|
action: 'accept' as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(responsesRepo.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
card: { id: 'card-1' },
|
||||||
|
user: { id: 'p-1' },
|
||||||
|
action: 'accept',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(responsesRepo.save).toHaveBeenCalled();
|
||||||
|
expect(evenementsService.maj).toHaveBeenCalledWith(
|
||||||
|
'p-1',
|
||||||
|
RoleType.PARENT,
|
||||||
|
'ev-1',
|
||||||
|
expect.objectContaining({ statut: StatutEvenementAgendaType.ACCEPTE }),
|
||||||
|
);
|
||||||
|
expect(cardsRepo.update).toHaveBeenCalledWith(
|
||||||
|
{ id: 'card-1' },
|
||||||
|
expect.objectContaining({ statut: CardInstanceStatutType.TRAITEE }),
|
||||||
|
);
|
||||||
|
expect(res.statut).toBe(CardInstanceStatutType.TRAITEE);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -227,12 +227,14 @@ export class CardsService {
|
|||||||
throw new BadRequestException('Motivation obligatoire en cas de refus');
|
throw new BadRequestException('Motivation obligatoire en cas de refus');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.responsesRepo.insert({
|
await this.responsesRepo.save(
|
||||||
id_card: card.id,
|
this.responsesRepo.create({
|
||||||
id_utilisateur: userId,
|
card: { id: card.id },
|
||||||
action: dto.action,
|
user: { id: userId },
|
||||||
comment: dto.comment?.trim() || undefined,
|
action: dto.action,
|
||||||
});
|
comment: dto.comment?.trim() || undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (card.id_evenement) {
|
if (card.id_evenement) {
|
||||||
if (dto.action === CardResponseActionType.ACCEPT || dto.action === CardResponseActionType.ACK) {
|
if (dto.action === CardResponseActionType.ACCEPT || dto.action === CardResponseActionType.ACK) {
|
||||||
@@ -252,8 +254,8 @@ export class CardsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
card.purge_at = this.purgeAt(card.type.retention_days, card.statut);
|
card.purge_at = this.purgeAt(card.type.retention_days, card.statut);
|
||||||
// update() et non save() : save() avec relations responses/audience
|
// Ne pas cardsRepo.save(card) avec relations chargées : TypeORM peut
|
||||||
// resynchronise les FK et tente UPDATE card_responses SET id_card=null.
|
// resynchroniser les FK enfants (UPDATE … SET id_card=null).
|
||||||
await this.cardsRepo.update(
|
await this.cardsRepo.update(
|
||||||
{ id: card.id },
|
{ id: card.id },
|
||||||
{ statut: card.statut, purge_at: card.purge_at },
|
{ statut: card.statut, purge_at: card.purge_at },
|
||||||
|
|||||||
Reference in New Issue
Block a user