Skip to content

Commit

Permalink
GH-449 - Creation of new AccountancyEntry now publishes an event.
Browse files Browse the repository at this point in the history
The creation of a new AccountancyEntry (custom or default) now causes an AccountancyEntryCreated event to be published, carrying the entry as payload. Listeners can selectively listen to events for particular entry types by declaring generics accordingly (see AccountancyEventPublicationTests).
  • Loading branch information
odrotbohm committed Nov 13, 2023
1 parent 084a172 commit 98cf55d
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@

import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

import javax.money.MonetaryAmount;

import org.jmolecules.ddd.types.Identifier;
import org.salespointframework.accountancy.AccountancyEntry.AccountancyEntryIdentifier;
import org.salespointframework.core.AbstractEntity;
import org.salespointframework.accountancy.AccountancyEvents.AccountancyEntryCreated;
import org.salespointframework.core.AbstractAggregateRoot;
import org.springframework.util.Assert;

/**
Expand All @@ -52,7 +52,7 @@
@ToString
@NoArgsConstructor(force = true, access = AccessLevel.PROTECTED, onConstructor = @__(@Deprecated))
@DiscriminatorColumn(length = 100)
public class AccountancyEntry extends AbstractEntity<AccountancyEntryIdentifier> {
public class AccountancyEntry extends AbstractAggregateRoot<AccountancyEntryIdentifier> {

private @EmbeddedId AccountancyEntryIdentifier accountancyEntryIdentifier = AccountancyEntryIdentifier
.of(UUID.randomUUID().toString());
Expand Down Expand Up @@ -83,6 +83,8 @@ public AccountancyEntry(MonetaryAmount value, String description) {

this.value = value;
this.description = description;

registerEvent(new AccountancyEntryCreated<>(this));
}

/**
Expand Down Expand Up @@ -146,7 +148,7 @@ void verifyConstraints() {
/**
* {@link AccountancyEntryIdentifier} serves as an identifier type and primary key for {@link AccountancyEntry}
* objects. The main reason for its existence is type safety for identifiers across the Salespoint framework. However,
* it can also be used as a key for non-persistent, {@link Map}-based implementations.
* it can also be used as a key for non-persistent, {@link java.util.Map}-based implementations.
*
* @author Hannes Weisbach
* @author Oliver Drotbohm
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.salespointframework.accountancy;

import lombok.Value;

import org.jmolecules.event.types.DomainEvent;
import org.springframework.core.ResolvableType;
import org.springframework.core.ResolvableTypeProvider;

/**
* All events published by the accountancy.
*
* @author Oliver Drotbohm
* @since 9.0.1
*/
public class AccountancyEvents {

/**
* Published whenever an {@link AccountancyEntry} is created within the system.
*
* @author Oliver Drotbohm
* @since 9.0.1
*/
@Value
public static class AccountancyEntryCreated<T extends AccountancyEntry>
implements DomainEvent, ResolvableTypeProvider {

AccountancyEntry entry;

/*
* (non-Javadoc)
* @see org.springframework.core.ResolvableTypeProvider#getResolvableType()
*/
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(AccountancyEntryCreated.class, entry.getClass());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.salespointframework.accountancy;

import static org.assertj.core.api.Assertions.*;

import jakarta.persistence.Entity;
import lombok.RequiredArgsConstructor;

import javax.money.MonetaryAmount;

import org.javamoney.moneta.Money;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.salespointframework.accountancy.AccountancyEventPublicationTests.CustomAccountancyEntryListener;
import org.salespointframework.accountancy.AccountancyEvents.AccountancyEntryCreated;
import org.salespointframework.core.Currencies;
import org.springframework.context.annotation.Import;
import org.springframework.context.event.EventListener;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.AssertablePublishedEvents;
import org.springframework.stereotype.Component;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.transaction.annotation.Transactional;

/**
* Integration tests related to event publication within the accountancy.
*
* @author Oliver Drotbohm
*/
@Transactional
@ApplicationModuleTest(extraIncludes = "org.salespointframework.time")
@RequiredArgsConstructor
@DirtiesContext
@Import(CustomAccountancyEntryListener.class)
class AccountancyEventPublicationTests {

private final Accountancy accountancy;
private final CustomAccountancyEntryListener listener;

@AfterEach
void afterEach() {
listener.event = null;
}

@Test // GH-449
void publishesEventForCreation(AssertablePublishedEvents events) {

var entry = accountancy.add(new AccountancyEntry(Money.of(10, Currencies.EURO)));

assertThat(events).contains(AccountancyEntryCreated.class)
.matching(it -> it.getEntry().equals(entry));

assertThat(listener.event).isNull();
}

@Test // GH-449
void listensToCustomEventsByGenericsDeclaration() {

var entry = accountancy.add(new CustomAccountancyEntry(Money.of(10, Currencies.EURO)));

assertThat(listener.event).isNotNull()
.satisfies(it -> {
assertThat(it.getEntry()).isEqualTo(entry);
});
}

@Component
static class CustomAccountancyEntryListener {

AccountancyEntryCreated<? extends AccountancyEntry> event;

@EventListener
void on(AccountancyEntryCreated<CustomAccountancyEntry> event) {
this.event = event;
}
}

@Entity
static class CustomAccountancyEntry extends AccountancyEntry {

public CustomAccountancyEntry(MonetaryAmount amount) {
super(amount, "Custom entry");
}
}
}

0 comments on commit 98cf55d

Please sign in to comment.