Skip to content

Commit

Permalink
Better fees calculation (#819)
Browse files Browse the repository at this point in the history
* do not include invalid transactions in the export

* better handling of application fees

* add extension for notifying in case of refunds

* update frontend version

* update view
  • Loading branch information
cbellone authored and syjer committed Oct 30, 2019
1 parent c375058 commit 0f1421c
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 14 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ junitVersion=5.1.0
systemProp.jdk.tls.client.protocols="TLSv1,TLSv1.1,TLSv1.2"

# https://jitpack.io/#alfio-event/alf.io-public-frontend -> go to commit tab, set the version
alfioPublicFrontendVersion=bd50a3bee2
alfioPublicFrontendVersion=b623050b82
16 changes: 15 additions & 1 deletion src/main/java/alfio/manager/ExtensionManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import alfio.model.system.ConfigurationKeys;
import alfio.repository.EventRepository;
import alfio.repository.TicketReservationRepository;
import alfio.repository.TransactionRepository;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
Expand All @@ -51,6 +52,8 @@ public class ExtensionManager {
private final TicketReservationRepository ticketReservationRepository;
private final NamedParameterJdbcTemplate jdbcTemplate;
private final ConfigurationManager configurationManager;
private final TransactionRepository transactionRepository;


public enum ExtensionEvent {
RESERVATION_CONFIRMED,
Expand All @@ -75,7 +78,8 @@ public enum ExtensionEvent {
STRIPE_CONNECT_STATE_GENERATION,

CONFIRMATION_MAIL_CUSTOM_TEXT,
TICKET_MAIL_CUSTOM_TEXT
TICKET_MAIL_CUSTOM_TEXT,
REFUND_ISSUED
}

void handleEventCreation(Event event) {
Expand All @@ -98,6 +102,8 @@ void handleReservationConfirmation(TicketReservation reservation, BillingDetails
Map<String, Object> payload = new HashMap<>();
payload.put("reservation", reservation);
payload.put("billingDetails", billingDetails);
transactionRepository.loadOptionalByReservationId(reservation.getId())
.ifPresent(tr -> payload.put("transaction", tr));
asyncCall(ExtensionEvent.RESERVATION_CONFIRMED,
event,
organizationId,
Expand Down Expand Up @@ -255,6 +261,14 @@ void handleReservationsCreditNoteIssuedForEvent(Event event, List<String> reserv
syncCall(ExtensionEvent.RESERVATION_CREDIT_NOTE_ISSUED, event, event.getOrganizationId(), payload, Boolean.class);
}

void handleRefund(Event event, TicketReservation reservation, TransactionAndPaymentInfo info) {
Map<String, Object> payload = new HashMap<>();
payload.put("reservation", reservation);
payload.put("transaction", info.getTransaction());
payload.put("paymentInfo", info.getPaymentInformation());
asyncCall(ExtensionEvent.REFUND_ISSUED, event, event.getOrganizationId(), payload);
}

public boolean handlePdfTransformation(String html, Event event, OutputStream outputStream) {
Map<String, Object> payload = new HashMap<>();
payload.put("html", html);
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/alfio/manager/PaymentManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class PaymentManager {
private final ConfigurationManager configurationManager;
private final AuditingRepository auditingRepository;
private final UserRepository userRepository;
private final ExtensionManager extensionManager;

private final List<PaymentProvider> paymentProviders; // injected by Spring

Expand Down Expand Up @@ -121,6 +122,7 @@ public boolean refund(TicketReservation reservation, Event event, Integer amount
event.getId(),
Audit.EventType.REFUND, new Date(), Audit.EntityType.RESERVATION, reservation.getId(),
Collections.singletonList(changes));
extensionManager.handleRefund(event, reservation, getInfo(reservation, event));
}

return res;
Expand Down
28 changes: 19 additions & 9 deletions src/main/java/alfio/manager/support/FeeCalculator.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,36 +27,46 @@
import java.util.function.BiFunction;

import static alfio.model.system.ConfigurationKeys.*;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static org.apache.commons.lang3.StringUtils.*;

public class FeeCalculator {
private final BigDecimal fee;
private final BigDecimal percentageFee;
private final BigDecimal minimumFee;
private final boolean percentage;
private final BigDecimal maximumFee;
private final boolean maxFeeDefined;
private final int numTickets;
private final String currencyCode;

private FeeCalculator(String feeAsString, String minimumFeeAsString, String currencyCode, int numTickets) {
this.percentage = feeAsString.endsWith("%");
this.fee = new BigDecimal(defaultIfEmpty(substringBefore(feeAsString, "%"), "0"));
private FeeCalculator(String feeAsString, String percentageFeeAsString, String minimumFeeAsString, String maxFeeAsString, String currencyCode, int numTickets) {
this.fee = new BigDecimal(defaultIfEmpty(trimToNull(feeAsString), "0"));
this.percentageFee = new BigDecimal(defaultIfEmpty(substringBefore(trimToNull(percentageFeeAsString), "%"), "0"));
this.minimumFee = new BigDecimal(defaultIfEmpty(trimToNull(minimumFeeAsString), "0"));
this.maximumFee = isEmpty(maxFeeAsString) ? null : new BigDecimal(trimToNull(maxFeeAsString));
this.maxFeeDefined = this.maximumFee != null;
this.numTickets = numTickets;
this.currencyCode = currencyCode;
}

private long calculate(long price) {
long result = percentage ? MonetaryUtil.calcPercentage(price, fee, BigDecimal::longValueExact) : MonetaryUtil.unitToCents(fee, currencyCode);
long percentage = MonetaryUtil.calcPercentage(price, percentageFee, BigDecimal::longValueExact);
long fixed = MonetaryUtil.unitToCents(fee, currencyCode) * numTickets;
long minFee = MonetaryUtil.unitToCents(minimumFee, currencyCode, BigDecimal::longValueExact) * numTickets;
return Math.max(result, minFee);
long maxFee = maxFeeDefined ? MonetaryUtil.unitToCents(maximumFee, currencyCode, BigDecimal::longValueExact) * numTickets : Long.MAX_VALUE;
return min(maxFee, max(percentage + fixed, minFee));
}

public static BiFunction<Integer, Long, Optional<Long>> getCalculator(EventAndOrganizationId event, ConfigurationManager configurationManager, String currencyCode) {
return (numTickets, amountInCent) -> {
if(isPlatformModeEnabled(event, configurationManager)) {
var fees = configurationManager.getFor(Set.of(PLATFORM_FEE, PLATFORM_MINIMUM_FEE), ConfigurationLevel.event(event));
String feeAsString = fees.get(PLATFORM_FEE).getValueOrDefault("0");
var fees = configurationManager.getFor(Set.of(PLATFORM_FIXED_FEE, PLATFORM_PERCENTAGE_FEE, PLATFORM_MINIMUM_FEE, PLATFORM_MAXIMUM_FEE), ConfigurationLevel.event(event));
String fixedFee = fees.get(PLATFORM_FIXED_FEE).getValueOrDefault("0");
String percentageFee = fees.get(PLATFORM_PERCENTAGE_FEE).getValueOrDefault("0");
String minimumFee = fees.get(PLATFORM_MINIMUM_FEE).getValueOrDefault("0");
return Optional.of(new FeeCalculator(feeAsString, minimumFee, currencyCode, numTickets).calculate(amountInCent));
String maximumFee = fees.get(PLATFORM_MAXIMUM_FEE).getValueOrDefault("");
return Optional.of(new FeeCalculator(fixedFee, percentageFee, minimumFee, maximumFee, currencyCode, numTickets).calculate(amountInCent));
}
return Optional.empty();
};
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/alfio/model/system/ConfigurationKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,12 @@ public enum ConfigurationKeys {

DEMO_MODE_ACCOUNT_EXPIRATION_DAYS("Account expiration days", false, SettingCategory.GENERAL, ComponentType.TEXT, false, EnumSet.of(SYSTEM)),
PLATFORM_MODE_ENABLED("Enable Platform mode", false, SettingCategory.PAYMENT, ComponentType.BOOLEAN, false, EnumSet.of(SYSTEM)),
PLATFORM_FEE("Platform fee to apply for each ticket sold (if you want to apply a percentage, just add '%' at the end). ", false, SettingCategory.PAYMENT, ComponentType.TEXT, false, EnumSet.of(SYSTEM)),
PLATFORM_MINIMUM_FEE("Platform minimum fee to apply to free tickets", false, SettingCategory.PAYMENT, ComponentType.TEXT, false, EnumSet.of(SYSTEM)),
@Deprecated
PLATFORM_FEE("Platform fee to apply for each ticket sold", true, SettingCategory.PAYMENT, ComponentType.TEXT, false, EnumSet.of(SYSTEM)),
PLATFORM_FIXED_FEE("Platform fee to apply for each ticket sold", true, SettingCategory.PAYMENT, ComponentType.TEXT, false, EnumSet.of(SYSTEM)),
PLATFORM_PERCENTAGE_FEE("Platform Percentage fee to apply for each ticket", false, SettingCategory.PAYMENT, ComponentType.TEXT, false, EnumSet.of(SYSTEM)),
PLATFORM_MINIMUM_FEE("Platform minimum fee to apply for each ticket", false, SettingCategory.PAYMENT, ComponentType.TEXT, false, EnumSet.of(SYSTEM)),
PLATFORM_MAXIMUM_FEE("Platform maximum fee to apply for each ticket", false, SettingCategory.PAYMENT, ComponentType.TEXT, false, EnumSet.of(SYSTEM)),
PAYMENT_METHODS_BLACKLIST("Payment methods blacklist. Comma-separated list of methods", false, SettingCategory.PAYMENT, ComponentType.TEXT, false, EnumSet.of(SYSTEM, TICKET_CATEGORY)),

STRIPE_CC_ENABLED("Stripe enabled", false, SettingCategory.PAYMENT_STRIPE, ComponentType.BOOLEAN, false, EnumSet.of(SYSTEM, ORGANIZATION)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
--
-- This file is part of alf.io.
--
-- alf.io is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- alf.io is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with alf.io. If not, see <http://www.gnu.org/licenses/>.
--

insert into configuration(c_key, c_value) select 'PLATFORM_PERCENTAGE_FEE', replace(c_value, '%', '') from configuration where c_key = 'PLATFORM_FEE' and c_value like '%\%';
insert into configuration(c_key, c_value) select 'PLATFORM_FIXED_FEE', c_value from configuration where c_key = 'PLATFORM_FEE' and c_value not like '%\%';
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ create view reservation_and_ticket_and_tx as (select

from tickets_reservation
left outer join ticket on tickets_reservation.id = ticket.tickets_reservation_id
left outer join b_transaction on ticket.tickets_reservation_id = b_transaction.reservation_id
left outer join b_transaction on ticket.tickets_reservation_id = b_transaction.reservation_id and b_transaction.status <> 'INVALID'
left outer join promo_code on tickets_reservation.promo_code_id_fk = promo_code.id
left outer join special_price on ticket.special_price_id_fk = special_price.id
);
118 changes: 118 additions & 0 deletions src/test/java/alfio/manager/support/FeeCalculatorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/
package alfio.manager.support;

import alfio.manager.system.ConfigurationManager;
import alfio.manager.system.ConfigurationManager.MaybeConfiguration;
import alfio.model.EventAndOrganizationId;
import alfio.model.system.ConfigurationKeyValuePathLevel;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static alfio.model.system.ConfigurationKeys.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class FeeCalculatorTest {

private EventAndOrganizationId event;
private ConfigurationManager configurationManager;

@BeforeEach
void setUp() {
event = mock(EventAndOrganizationId.class);
configurationManager = mock(ConfigurationManager.class);
when(configurationManager.getFor(eq(PLATFORM_MODE_ENABLED), any())).thenReturn(new MaybeConfiguration(PLATFORM_MODE_ENABLED, new ConfigurationKeyValuePathLevel(PLATFORM_MODE_ENABLED.name(), "true", null)));
}

@Test
void calculatePercentageFee() {
initFeesConfiguration(null, "5", null, null);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(1, 100_00L), 5_00L);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(5, 500_00L), 25_00L);
}

@Test
void calculatePercentageFeeWithPercentSign() {
initFeesConfiguration(null, "5%", null, null);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(1, 100_00L), 5_00L);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(5, 500_00L), 25_00L);
}

@Test
void calculateFixedFee() {
initFeesConfiguration("10", null, null, null);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(1, 1_00L), 10_00L);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(1, 1_00L), 10_00L);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(1, 1_00L), 10_00L);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(5, 5_00L), 50_00L);
}

@Test
void calculatePercentageAndFixedFee() {
initFeesConfiguration("10", "5", null, null);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(1, 100_00L), 15_00L);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(5, 500_00L), 75_00L);
}

@Test
void applyMinimumFee() {
initFeesConfiguration(null, "1", "0.5", null);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(1, 10_00L), 50L);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(5, 500_00L), 5_00L);
}

@Test
void applyMaximumFee() {
initFeesConfiguration(null, "1", null, "2");
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(1, 10_00L), 10L);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(5, 500_00L), 5_00L);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(1, 500_00L), 2_00L);
}

@Test
void testCombinations() {
initFeesConfiguration("0.5", "3", null, "20");
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(1, 100_00L), 3_50L);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(1, 1000_00L), 20_00L);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(1, 834_00L), 20_00L);
expectFeeToBe(FeeCalculator.getCalculator(event, configurationManager, "CHF").apply(2, 1680_00L), 40_00L);
}

private void initFeesConfiguration(String fixedFee, String percentageFee, String minimumFee, String maximumFee) {
when(configurationManager.getFor(eq(Set.of(PLATFORM_FIXED_FEE, PLATFORM_PERCENTAGE_FEE, PLATFORM_MINIMUM_FEE, PLATFORM_MAXIMUM_FEE)), any()))
.thenReturn(Map.of(
PLATFORM_FIXED_FEE, new MaybeConfiguration(PLATFORM_FIXED_FEE, new ConfigurationKeyValuePathLevel(null, fixedFee, null)),
PLATFORM_PERCENTAGE_FEE, new MaybeConfiguration(PLATFORM_PERCENTAGE_FEE, new ConfigurationKeyValuePathLevel(null, percentageFee, null)),
PLATFORM_MINIMUM_FEE, new MaybeConfiguration(PLATFORM_MINIMUM_FEE, new ConfigurationKeyValuePathLevel(null, minimumFee, null)),
PLATFORM_MAXIMUM_FEE, new MaybeConfiguration(PLATFORM_MAXIMUM_FEE, new ConfigurationKeyValuePathLevel(null, maximumFee, null))
));
}

private void expectFeeToBe(Optional<Long> fee, Long value) {
assertNotNull(fee);
assertTrue(fee.isPresent());
assertEquals(value, fee.get());
}
}

0 comments on commit 0f1421c

Please sign in to comment.