feat(api): complete domain event publishing with aggregate root pattern
- Add getUncommittedEvents() and commit() to AggregateRoot base class - Create 6 new domain events: SubscriptionExpired, SubscriptionRenewed, ListingStatusChanged, UserKycUpdated, UserDeactivated, PaymentRefunded - Wire events into entity state changes: SubscriptionEntity (markExpired, renewPeriod), ListingEntity (all transitions), UserEntity (KYC, deactivate), PaymentEntity (markRefunded) - Add 7 new event listeners across notifications, admin, and search modules (25 total @OnEvent handlers) - Fix ReviewDeletedListener to handle LISTING target type - Restore watcher notifications in ListingSoldListener - Update barrel exports and module registrations Resolves: TEC-1564 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
|
||||
import { PaymentCompletedEvent } from '../events/payment-completed.event';
|
||||
import { PaymentCreatedEvent } from '../events/payment-created.event';
|
||||
import { PaymentFailedEvent } from '../events/payment-failed.event';
|
||||
import { PaymentRefundedEvent } from '../events/payment-refunded.event';
|
||||
|
||||
describe('Payment Domain Events', () => {
|
||||
describe('PaymentCreatedEvent', () => {
|
||||
@@ -59,4 +60,17 @@ describe('Payment Domain Events', () => {
|
||||
expect(event.occurredAt).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PaymentRefundedEvent', () => {
|
||||
it('creates event with correct properties', () => {
|
||||
const event = new PaymentRefundedEvent('payment-1', 'user-1', 'VNPAY', 500_000n);
|
||||
|
||||
expect(event.eventName).toBe('payment.refunded');
|
||||
expect(event.aggregateId).toBe('payment-1');
|
||||
expect(event.userId).toBe('user-1');
|
||||
expect(event.provider).toBe('VNPAY');
|
||||
expect(event.amountVND).toBe(500_000n);
|
||||
expect(event.occurredAt).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { PaymentEntity } from '../entities/payment.entity';
|
||||
import { PaymentCompletedEvent } from '../events/payment-completed.event';
|
||||
import { PaymentCreatedEvent } from '../events/payment-created.event';
|
||||
import { PaymentFailedEvent } from '../events/payment-failed.event';
|
||||
import { PaymentRefundedEvent } from '../events/payment-refunded.event';
|
||||
import { Money } from '../value-objects/money.vo';
|
||||
|
||||
describe('PaymentEntity', () => {
|
||||
@@ -107,11 +108,16 @@ describe('PaymentEntity', () => {
|
||||
expect(result.unwrapErr().message).toContain('Cannot fail payment');
|
||||
});
|
||||
|
||||
it('should mark completed payment as refunded', () => {
|
||||
it('should mark completed payment as refunded and emit event', () => {
|
||||
const payment = createPayment('COMPLETED');
|
||||
payment.clearDomainEvents();
|
||||
const result = payment.markRefunded();
|
||||
expect(result.isOk).toBe(true);
|
||||
expect(payment.status).toBe('REFUNDED');
|
||||
|
||||
const events = payment.domainEvents;
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]).toBeInstanceOf(PaymentRefundedEvent);
|
||||
});
|
||||
|
||||
it('should not refund a non-completed payment', () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AggregateRoot, DomainException, ErrorCode, Result } from '@modules/shar
|
||||
import { PaymentCompletedEvent } from '../events/payment-completed.event';
|
||||
import { PaymentCreatedEvent } from '../events/payment-created.event';
|
||||
import { PaymentFailedEvent } from '../events/payment-failed.event';
|
||||
import { PaymentRefundedEvent } from '../events/payment-refunded.event';
|
||||
import { type Money } from '../value-objects/money.vo';
|
||||
|
||||
export interface PaymentProps {
|
||||
@@ -156,6 +157,10 @@ export class PaymentEntity extends AggregateRoot<string> {
|
||||
}
|
||||
this._status = 'REFUNDED';
|
||||
this.updatedAt = new Date();
|
||||
|
||||
this.addDomainEvent(
|
||||
new PaymentRefundedEvent(this.id, this._userId, this._provider, this._amount.value),
|
||||
);
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { type PaymentProvider } from '@prisma/client';
|
||||
import { type DomainEvent } from '@modules/shared';
|
||||
|
||||
export class PaymentRefundedEvent implements DomainEvent {
|
||||
readonly eventName = 'payment.refunded';
|
||||
readonly occurredAt = new Date();
|
||||
|
||||
constructor(
|
||||
public readonly aggregateId: string,
|
||||
public readonly userId: string,
|
||||
public readonly provider: PaymentProvider,
|
||||
public readonly amountVND: bigint,
|
||||
) {}
|
||||
}
|
||||
@@ -2,3 +2,5 @@ export { PaymentsModule } from './payments.module';
|
||||
export { PAYMENT_REPOSITORY, type IPaymentRepository } from './domain/repositories/payment.repository';
|
||||
export { PAYMENT_GATEWAY_FACTORY, type IPaymentGatewayFactory } from './infrastructure/services/payment-gateway.interface';
|
||||
export { PaymentCompletedEvent } from './domain/events/payment-completed.event';
|
||||
export { PaymentFailedEvent } from './domain/events/payment-failed.event';
|
||||
export { PaymentRefundedEvent } from './domain/events/payment-refunded.event';
|
||||
|
||||
Reference in New Issue
Block a user