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:
Ho Ngoc Hai
2026-04-09 10:22:20 +07:00
parent 35feccb529
commit 8179f1c16e
37 changed files with 613 additions and 36 deletions

View File

@@ -1 +1,2 @@
export { ListingApprovedEventHandler } from './listing-approved.handler';
export { ListingStatusChangedHandler } from './listing-status-changed.handler';

View File

@@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { type ListingStatusChangedEvent } from '@modules/listings';
import { CacheService, CachePrefix, type LoggerService } from '@modules/shared';
import { type ListingIndexerService } from '../services/listing-indexer.service';
@Injectable()
export class ListingStatusChangedHandler {
constructor(
private readonly indexer: ListingIndexerService,
private readonly cache: CacheService,
private readonly logger: LoggerService,
) {}
@OnEvent('listing.status_changed', { async: true })
async handle(event: ListingStatusChangedEvent): Promise<void> {
this.logger.log(
`Handling listing.status_changed: ${event.previousStatus}${event.newStatus} for ${event.aggregateId}`,
'ListingStatusChangedHandler',
);
// Remove from search index when listing becomes inactive
const removeStatuses = ['REJECTED', 'EXPIRED', 'SOLD', 'RENTED'];
if (removeStatuses.includes(event.newStatus)) {
await this.indexer.removeListing(event.aggregateId);
}
// Invalidate caches for any status change
await Promise.all([
this.cache.invalidate(CacheService.buildKey(CachePrefix.LISTING, event.aggregateId)),
this.cache.invalidateByPrefix(CachePrefix.SEARCH),
this.cache.invalidateByPrefix(CachePrefix.GEO_SEARCH),
]);
}
}

View File

@@ -7,6 +7,7 @@ import { GeoSearchHandler } from './application/queries/geo-search/geo-search.ha
import { SearchPropertiesHandler } from './application/queries/search-properties/search-properties.handler';
import { SEARCH_REPOSITORY } from './domain/repositories/search.repository';
import { ListingApprovedEventHandler } from './infrastructure/event-handlers/listing-approved.handler';
import { ListingStatusChangedHandler } from './infrastructure/event-handlers/listing-status-changed.handler';
import { ListingIndexerService } from './infrastructure/services/listing-indexer.service';
import { TypesenseClientService } from './infrastructure/services/typesense-client.service';
import { TypesenseSearchRepository } from './infrastructure/services/typesense-search.repository';
@@ -27,6 +28,7 @@ const QueryHandlers = [SearchPropertiesHandler, GeoSearchHandler];
// Event handlers
ListingApprovedEventHandler,
ListingStatusChangedHandler,
// CQRS
...CommandHandlers,