Skip to content

Commit 36e0f9f

Browse files
authored
Implement domain event publication in financial-accounting service (#402)
* feat: implement LedgerPostingCapturedEvent publication Implement event publication for LedgerPostingCapturedEvent in CaptureLedgerPosting() method after successful SavePosting() call. Changes: - Add imports for eventsv1 and timestamppb - Update DomainEvent type to be proto.Message for protobuf compatibility - Publish LedgerPostingCapturedEvent with all required fields: * PostingId, BookingLogId, PostingDirection, PostingAmount * AccountId, ValueDate, Status * CorrelationId, CausationId, Timestamp, Version - Use best-effort error handling (log errors, don't fail operation) Follows existing pattern from payment-order service for protobuf event publication. Event publishing enables inter-service coordination and audit trail for ledger postings. * feat: implement LedgerPostingAmendedEvent publication in UpdateLedgerPosting Implement event publication for LedgerPostingAmendedEvent in UpdateLedgerPosting() method after successful UpdatePosting() call. Changes: - Extract correlation ID from idempotency key for event tracing - Capture previous amount and status BEFORE update operation - Publish LedgerPostingAmendedEvent with all required fields: * PostingId, BookingLogId * PreviousAmount, NewAmount (same value for status transitions) * Reason (status change description), AmendedBy (system) * CorrelationId, CausationId, Timestamp, Version - Use best-effort error handling (log errors, don't fail operation) - Remove TODO comment Note: UpdateLedgerPosting changes status, not amount. Event captures amount for audit purposes even though it doesn't change during status transitions. The Reason field describes the status change. Follows existing pattern from LedgerPostingCapturedEvent for protobuf event publication. Event publishing enables inter-service coordination and audit trail for ledger posting modifications. * feat: publish FinancialBookingLogInitiatedEvent in InitiateFinancialBookingLog Implement event publication for FinancialBookingLogInitiatedEvent after successful SaveBookingLog() call. The event captures booking log creation with all required fields including booking log ID, account type, product service reference, business unit reference, and base currency. Event publishing follows the existing best-effort pattern - errors are logged but do not fail the operation. Uses helper functions toProtoAccountType() and toProtoCurrency() for domain-to-proto translation. This enables inter-service coordination for financial booking log lifecycle tracking. * feat: implement FinancialBookingLogUpdatedEvent publication Implements event publication for FinancialBookingLogUpdatedEvent in UpdateFinancialBookingLog() method with proper previous status capture. Changes: - Capture previous status BEFORE repository update operation - Publish FinancialBookingLogUpdatedEvent after successful update - Include all required fields: status, previous_status, correlation_id, chart_of_accounts_rules, reason, updated_by, timestamp, version - Follow best-effort pattern: log errors but don't fail operation - Use existing toProtoTransactionStatus() converter Related: Task 18.4 - Domain events implementation --------- Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent 658ac49 commit 36e0f9f

1 file changed

Lines changed: 120 additions & 21 deletions

File tree

services/financial-accounting/service/financial_accounting_service.go

Lines changed: 120 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import (
1111
"google.golang.org/grpc/codes"
1212
"google.golang.org/grpc/status"
1313
"google.golang.org/protobuf/proto"
14+
"google.golang.org/protobuf/types/known/timestamppb"
1415

1516
commonv1 "github.com/meridianhub/meridian/api/proto/meridian/common/v1"
17+
eventsv1 "github.com/meridianhub/meridian/api/proto/meridian/events/v1"
1618
financialaccountingv1 "github.com/meridianhub/meridian/api/proto/meridian/financial_accounting/v1"
1719
"github.com/meridianhub/meridian/services/financial-accounting/adapters/persistence"
1820
"github.com/meridianhub/meridian/services/financial-accounting/domain"
@@ -37,22 +39,20 @@ var (
3739
)
3840

3941
// DomainEvent is a marker interface for all financial accounting domain events.
40-
// Concrete event types will be defined in domain/events.go in subsequent subtasks.
42+
// In practice, events are protobuf messages from eventsv1 package.
43+
// The interface uses proto.Message for maximum compatibility with protobuf events.
4144
//
42-
// Event types to be implemented (subtask 9.2+):
43-
// - LedgerPostingCapturedEvent
44-
// - LedgerPostingAmendedEvent
45-
// - LedgerPostingPostedEvent
46-
// - LedgerPostingRejectedEvent
47-
// - FinancialBookingLogInitiatedEvent
48-
// - FinancialBookingLogUpdatedEvent
49-
// - FinancialBookingLogPostedEvent
50-
// - FinancialBookingLogClosedEvent
51-
// - BalanceValidationFailedEvent
52-
type DomainEvent interface {
53-
// EventType returns the type identifier for this event
54-
EventType() string
55-
}
45+
// Event types (protobuf-based):
46+
// - eventsv1.LedgerPostingCapturedEvent
47+
// - eventsv1.LedgerPostingAmendedEvent
48+
// - eventsv1.LedgerPostingPostedEvent
49+
// - eventsv1.LedgerPostingRejectedEvent
50+
// - eventsv1.FinancialBookingLogInitiatedEvent
51+
// - eventsv1.FinancialBookingLogUpdatedEvent
52+
// - eventsv1.FinancialBookingLogPostedEvent
53+
// - eventsv1.FinancialBookingLogClosedEvent
54+
// - eventsv1.BalanceValidationFailedEvent
55+
type DomainEvent = proto.Message
5656

5757
// EventPublisher defines the interface for publishing domain events to the messaging infrastructure.
5858
// Events are published to Kafka following ADR-0004 (Event Schema Evolution Strategy).
@@ -258,8 +258,27 @@ func (s *FinancialAccountingService) CaptureLedgerPosting(
258258
return nil, status.Errorf(codes.Internal, "failed to save posting: %v", err)
259259
}
260260

261-
// TODO(75-async-audit#5): Implement LedgerPostingCapturedEvent and publish it
262-
// Will use Kafka audit events with outbox pattern for guaranteed delivery
261+
// Publish LedgerPostingCapturedEvent for inter-service coordination
262+
// Event publishing is best-effort - errors are logged but don't fail the operation
263+
event := &eventsv1.LedgerPostingCapturedEvent{
264+
PostingId: posting.ID.String(),
265+
BookingLogId: posting.FinancialBookingLogID.String(),
266+
PostingDirection: toProtoPostingDirection(posting.Direction),
267+
PostingAmount: toProtoMoney(posting.Amount),
268+
AccountId: posting.AccountID,
269+
ValueDate: timestamppb.New(posting.ValueDate),
270+
Status: toProtoTransactionStatus(posting.Status),
271+
CorrelationId: correlationID,
272+
CausationId: correlationID, // Request caused this event
273+
Timestamp: timestamppb.Now(),
274+
Version: 1, // Initial version for newly created posting
275+
}
276+
if err := s.eventPublisher.Publish(ctx, event); err != nil {
277+
slog.Error("failed to publish LedgerPostingCapturedEvent",
278+
"error", err,
279+
"posting_id", posting.ID.String(),
280+
"booking_log_id", posting.FinancialBookingLogID.String())
281+
}
263282

264283
// Convert to proto response
265284
response := &financialaccountingv1.CaptureLedgerPostingResponse{
@@ -639,6 +658,12 @@ func (s *FinancialAccountingService) UpdateLedgerPosting(
639658
}
640659
newStatus := fromProtoTransactionStatus(req.Status)
641660

661+
// Extract correlation ID from idempotency key
662+
correlationID := ""
663+
if req.IdempotencyKey != nil {
664+
correlationID = req.IdempotencyKey.Key
665+
}
666+
642667
// Retrieve existing posting
643668
posting, err := s.repository.GetPosting(ctx, postingID)
644669
if err != nil {
@@ -648,6 +673,10 @@ func (s *FinancialAccountingService) UpdateLedgerPosting(
648673
return nil, status.Errorf(codes.Internal, "failed to retrieve posting: %v", err)
649674
}
650675

676+
// Capture previous state BEFORE any modifications (for LedgerPostingAmendedEvent)
677+
previousAmount := posting.Amount
678+
previousStatus := posting.Status
679+
651680
// Validate and apply state transition using domain methods
652681
postingResult := req.PostingResult
653682
if postingResult == "" {
@@ -699,8 +728,29 @@ func (s *FinancialAccountingService) UpdateLedgerPosting(
699728
return nil, status.Errorf(codes.Internal, "failed to update posting: %v", err)
700729
}
701730

702-
// Publish domain event (placeholder - actual event will be implemented in event subtask)
703-
// TODO(75-async-audit#5): Implement LedgerPostingUpdatedEvent and publish it
731+
// Publish LedgerPostingAmendedEvent for inter-service coordination
732+
// Event publishing is best-effort - errors are logged but don't fail the operation
733+
// Note: UpdateLedgerPosting changes status, not amount. Both previous_amount and new_amount
734+
// will be the same value since amount doesn't change in status transitions.
735+
event := &eventsv1.LedgerPostingAmendedEvent{
736+
PostingId: posting.ID.String(),
737+
BookingLogId: posting.FinancialBookingLogID.String(),
738+
PreviousAmount: toProtoMoney(previousAmount),
739+
NewAmount: toProtoMoney(posting.Amount),
740+
Reason: fmt.Sprintf("Status updated from %v to %v", previousStatus, newStatus),
741+
AmendedBy: "system", // Status transitions are system-driven
742+
CorrelationId: correlationID,
743+
CausationId: correlationID, // Request caused this event
744+
Timestamp: timestamppb.Now(),
745+
Version: 1, // Increment version for optimistic locking
746+
}
747+
if err := s.eventPublisher.Publish(ctx, event); err != nil {
748+
slog.Error("failed to publish LedgerPostingAmendedEvent",
749+
"error", err,
750+
"posting_id", posting.ID.String(),
751+
"booking_log_id", posting.FinancialBookingLogID.String(),
752+
"status", newStatus)
753+
}
704754

705755
// Convert to proto response
706756
response := &financialaccountingv1.UpdateLedgerPostingResponse{
@@ -853,7 +903,28 @@ func (s *FinancialAccountingService) InitiateFinancialBookingLog(
853903
return nil, status.Errorf(codes.Internal, "failed to save booking log: %v", err)
854904
}
855905

856-
// TODO(75-async-audit#5): Publish FinancialBookingLogInitiatedEvent for inter-service coordination
906+
// Publish FinancialBookingLogInitiatedEvent for inter-service coordination
907+
// Event publishing is best-effort - errors are logged but don't fail the operation
908+
correlationID := ""
909+
if req.IdempotencyKey != nil {
910+
correlationID = req.IdempotencyKey.Key
911+
}
912+
event := &eventsv1.FinancialBookingLogInitiatedEvent{
913+
BookingLogId: bookingLog.ID.String(),
914+
FinancialAccountType: toProtoAccountType(bookingLog.FinancialAccountType),
915+
ProductServiceReference: bookingLog.ProductServiceReference,
916+
BusinessUnitReference: bookingLog.BusinessUnitReference,
917+
BaseCurrency: toProtoCurrency(bookingLog.BaseCurrency),
918+
CorrelationId: correlationID,
919+
CausationId: correlationID, // Request caused this event
920+
Timestamp: timestamppb.Now(),
921+
Version: 1, // Initial version for newly created booking log
922+
}
923+
if err := s.eventPublisher.Publish(ctx, event); err != nil {
924+
slog.Error("failed to publish FinancialBookingLogInitiatedEvent",
925+
"error", err,
926+
"booking_log_id", bookingLog.ID.String())
927+
}
857928

858929
// Store idempotency result (only if service configured)
859930
if s.idempotency != nil {
@@ -1002,6 +1073,9 @@ func (s *FinancialAccountingService) UpdateFinancialBookingLog(
10021073
return nil, status.Errorf(codes.Internal, "failed to retrieve booking log: %v", err)
10031074
}
10041075

1076+
// Capture previous status BEFORE update for event publishing
1077+
previousStatus := bookingLog.Status
1078+
10051079
// Validate state transition using the state machine
10061080
// This handles all valid transitions including POSTED -> REVERSED for reversals
10071081
if !isValidBookingLogTransition(bookingLog.Status, newStatus) {
@@ -1087,7 +1161,32 @@ func (s *FinancialAccountingService) UpdateFinancialBookingLog(
10871161
return nil, status.Errorf(codes.Internal, "failed to update booking log: %v", err)
10881162
}
10891163

1090-
// TODO(75-async-audit#5): Publish FinancialBookingLogUpdatedEvent for inter-service coordination
1164+
// Publish FinancialBookingLogUpdatedEvent for inter-service coordination
1165+
// Event publishing is best-effort - errors are logged but don't fail the operation
1166+
correlationID := ""
1167+
if req.IdempotencyKey != nil {
1168+
correlationID = req.IdempotencyKey.Key
1169+
}
1170+
event := &eventsv1.FinancialBookingLogUpdatedEvent{
1171+
BookingLogId: bookingLogID.String(),
1172+
Status: toProtoTransactionStatus(newStatus),
1173+
PreviousStatus: toProtoTransactionStatus(previousStatus),
1174+
ChartOfAccountsRules: updated.ChartOfAccountsRules,
1175+
Reason: fmt.Sprintf("Status updated from %s to %s", previousStatus, newStatus),
1176+
UpdatedBy: "system", // TODO: Extract from auth context when available
1177+
CorrelationId: correlationID,
1178+
CausationId: correlationID, // Request caused this event
1179+
Timestamp: timestamppb.Now(),
1180+
Version: 1, // Version tracking would need to be added to domain model
1181+
}
1182+
1183+
if err := s.eventPublisher.Publish(ctx, event); err != nil {
1184+
slog.Error("failed to publish FinancialBookingLogUpdatedEvent",
1185+
"error", err,
1186+
"booking_log_id", bookingLogID.String(),
1187+
"previous_status", previousStatus,
1188+
"new_status", newStatus)
1189+
}
10911190

10921191
// Convert to proto response
10931192
response := &financialaccountingv1.UpdateFinancialBookingLogResponse{

0 commit comments

Comments
 (0)