-
-
Notifications
You must be signed in to change notification settings - Fork 740
/
BlockingCalculator.java
314 lines (268 loc) · 14.1 KB
/
BlockingCalculator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
/*
* Copyright 2010-2013 Ning, Inc.
*
* Ning licenses this file to you 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:
*
* http://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.killbill.billing.junction.plumbing.billing;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.entitlement.api.BlockingState;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.junction.BlockingInternalApi;
import org.killbill.billing.subscription.api.SubscriptionBase;
import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import com.google.common.annotations.VisibleForTesting;
import com.google.inject.Inject;
public class BlockingCalculator {
private static final AtomicLong globaltotalOrder = new AtomicLong();
private final BlockingInternalApi blockingApi;
protected static class DisabledDuration {
private final DateTime start;
private DateTime end;
public DisabledDuration(final DateTime start, final DateTime end) {
this.start = start;
this.end = end;
}
public DateTime getStart() {
return start;
}
public DateTime getEnd() {
return end;
}
public void setEnd(final DateTime end) {
this.end = end;
}
}
@Inject
public BlockingCalculator(final BlockingInternalApi blockingApi) {
this.blockingApi = blockingApi;
}
/**
* Given a set of billing events, add corresponding blocking (overdue) billing events.
*
* @param billingEvents the original list of billing events to update (without overdue events)
*/
public void insertBlockingEvents(final SortedSet<BillingEvent> billingEvents, final InternalTenantContext context) {
if (billingEvents.size() <= 0) {
return;
}
final Account account = billingEvents.first().getAccount();
final Hashtable<UUID, List<SubscriptionBase>> bundleMap = createBundleSubscriptionMap(billingEvents);
final SortedSet<BillingEvent> billingEventsToAdd = new TreeSet<BillingEvent>();
final SortedSet<BillingEvent> billingEventsToRemove = new TreeSet<BillingEvent>();
final List<BlockingState> blockingEvents = blockingApi.getBlockingAllForAccount(context);
final List<DisabledDuration> blockingDurations = createBlockingDurations(blockingEvents);
for (final UUID bundleId : bundleMap.keySet()) {
for (final SubscriptionBase subscription : bundleMap.get(bundleId)) {
billingEventsToAdd.addAll(createNewEvents(blockingDurations, billingEvents, account, subscription));
billingEventsToRemove.addAll(eventsToRemove(blockingDurations, billingEvents, subscription));
}
}
for (final BillingEvent eventToAdd : billingEventsToAdd) {
billingEvents.add(eventToAdd);
}
for (final BillingEvent eventToRemove : billingEventsToRemove) {
billingEvents.remove(eventToRemove);
}
}
protected SortedSet<BillingEvent> eventsToRemove(final List<DisabledDuration> disabledDuration,
final SortedSet<BillingEvent> billingEvents, final SubscriptionBase subscription) {
final SortedSet<BillingEvent> result = new TreeSet<BillingEvent>();
final SortedSet<BillingEvent> filteredBillingEvents = filter(billingEvents, subscription);
for (final DisabledDuration duration : disabledDuration) {
for (final BillingEvent event : filteredBillingEvents) {
if (duration.getEnd() == null || event.getEffectiveDate().isBefore(duration.getEnd())) {
if (event.getEffectiveDate().isAfter(duration.getStart())) { //between the pair
result.add(event);
}
} else { //after the last event of the pair no need to keep checking
break;
}
}
}
return result;
}
protected SortedSet<BillingEvent> createNewEvents(final List<DisabledDuration> disabledDuration, final SortedSet<BillingEvent> billingEvents, final Account account, final SubscriptionBase subscription) {
final SortedSet<BillingEvent> result = new TreeSet<BillingEvent>();
for (final DisabledDuration duration : disabledDuration) {
// The first one before the blocked duration
final BillingEvent precedingInitialEvent = precedingBillingEventForSubscription(duration.getStart(), billingEvents, subscription);
// The last one during of before the duration
final BillingEvent precedingFinalEvent = precedingBillingEventForSubscription(duration.getEnd(), billingEvents, subscription);
if (precedingInitialEvent != null) { // there is a preceding billing event
result.add(createNewDisableEvent(duration.getStart(), precedingInitialEvent));
if (duration.getEnd() != null) { // no second event in the pair means they are still disabled (no re-enable)
result.add(createNewReenableEvent(duration.getEnd(), precedingFinalEvent));
}
} else if (precedingFinalEvent != null) { // can happen - e.g. phase event
result.add(createNewReenableEvent(duration.getEnd(), precedingFinalEvent));
}
// N.B. if there's no precedingInitial and no precedingFinal then there's nothing to do
}
return result;
}
protected BillingEvent precedingBillingEventForSubscription(final DateTime datetime, final SortedSet<BillingEvent> billingEvents, final SubscriptionBase subscription) {
if (datetime == null) { //second of a pair can be null if there's no re-enabling
return null;
}
final SortedSet<BillingEvent> filteredBillingEvents = filter(billingEvents, subscription);
BillingEvent result = filteredBillingEvents.first();
if (datetime.isBefore(result.getEffectiveDate())) {
//This case can happen, for example, if we have an add on and the bundle goes into disabled before the add on is created
return null;
}
for (final BillingEvent event : filteredBillingEvents) {
if (!event.getEffectiveDate().isBefore(datetime)) { // found it its the previous event
return result;
} else { // still looking
result = event;
}
}
return result;
}
protected SortedSet<BillingEvent> filter(final SortedSet<BillingEvent> billingEvents, final SubscriptionBase subscription) {
final SortedSet<BillingEvent> result = new TreeSet<BillingEvent>();
for (final BillingEvent event : billingEvents) {
if (event.getSubscription() == subscription) {
result.add(event);
}
}
return result;
}
protected BillingEvent createNewDisableEvent(final DateTime odEventTime, final BillingEvent previousEvent) {
final Account account = previousEvent.getAccount();
final int billCycleDay = previousEvent.getBillCycleDayLocal();
final SubscriptionBase subscription = previousEvent.getSubscription();
final DateTime effectiveDate = odEventTime;
final PlanPhase planPhase = previousEvent.getPlanPhase();
final Plan plan = previousEvent.getPlan();
// Make sure to set the fixed price to null and the billing period to NO_BILLING_PERIOD,
// which makes invoice disregard this event
final BigDecimal fixedPrice = null;
final BigDecimal recurringPrice = null;
final BillingPeriod billingPeriod = BillingPeriod.NO_BILLING_PERIOD;
final Currency currency = previousEvent.getCurrency();
final String description = "";
final BillingMode billingMode = previousEvent.getBillingMode();
final SubscriptionBaseTransitionType type = SubscriptionBaseTransitionType.START_BILLING_DISABLED;
final Long totalOrdering = globaltotalOrder.getAndIncrement();
final DateTimeZone tz = previousEvent.getTimeZone();
return new DefaultBillingEvent(account, subscription, effectiveDate, plan, planPhase,
fixedPrice, recurringPrice, currency,
billingPeriod, billCycleDay, billingMode,
description, totalOrdering, type, tz);
}
protected BillingEvent createNewReenableEvent(final DateTime odEventTime, final BillingEvent previousEvent) {
// All fields are populated with the event state from before the blocking period, for invoice to resume invoicing
final Account account = previousEvent.getAccount();
final int billCycleDay = previousEvent.getBillCycleDayLocal();
final SubscriptionBase subscription = previousEvent.getSubscription();
final DateTime effectiveDate = odEventTime;
final PlanPhase planPhase = previousEvent.getPlanPhase();
final Plan plan = previousEvent.getPlan();
final BigDecimal fixedPrice = previousEvent.getFixedPrice();
final BigDecimal recurringPrice = previousEvent.getRecurringPrice();
final Currency currency = previousEvent.getCurrency();
final String description = "";
final BillingMode billingMode = previousEvent.getBillingMode();
final BillingPeriod billingPeriod = previousEvent.getBillingPeriod();
final SubscriptionBaseTransitionType type = SubscriptionBaseTransitionType.END_BILLING_DISABLED;
final Long totalOrdering = globaltotalOrder.getAndIncrement();
final DateTimeZone tz = previousEvent.getTimeZone();
return new DefaultBillingEvent(account, subscription, effectiveDate, plan, planPhase,
fixedPrice, recurringPrice, currency,
billingPeriod, billCycleDay, billingMode,
description, totalOrdering, type, tz);
}
protected Hashtable<UUID, List<SubscriptionBase>> createBundleSubscriptionMap(final SortedSet<BillingEvent> billingEvents) {
final Hashtable<UUID, List<SubscriptionBase>> result = new Hashtable<UUID, List<SubscriptionBase>>();
for (final BillingEvent event : billingEvents) {
final UUID bundleId = event.getSubscription().getBundleId();
List<SubscriptionBase> subs = result.get(bundleId);
if (subs == null) {
subs = new ArrayList<SubscriptionBase>();
result.put(bundleId, subs);
}
if (!result.get(bundleId).contains(event.getSubscription())) {
subs.add(event.getSubscription());
}
}
return result;
}
// In ascending order
protected List<DisabledDuration> createBlockingDurations(final Iterable<BlockingState> overdueBundleEvents) {
final List<DisabledDuration> result = new ArrayList<BlockingCalculator.DisabledDuration>();
// Earliest blocking event
BlockingState first = null;
int blockedNesting = 0;
BlockingState lastOne = null;
for (final BlockingState e : overdueBundleEvents) {
lastOne = e;
if (e.isBlockBilling() && blockedNesting == 0) {
// First blocking event of contiguous series of blocking events
first = e;
blockedNesting++;
} else if (e.isBlockBilling() && blockedNesting > 0) {
// Nest blocking states
blockedNesting++;
} else if (!e.isBlockBilling() && blockedNesting > 0) {
blockedNesting--;
if (blockedNesting == 0) {
// End of the interval
addDisabledDuration(result, first, e);
first = null;
}
}
}
if (first != null) { // found a transition to disabled with no terminating event
addDisabledDuration(result, first, lastOne.isBlockBilling() ? null : lastOne);
}
return result;
}
private void addDisabledDuration(final List<DisabledDuration> result, final BlockingState firstBlocking, @Nullable final BlockingState firstNonBlocking) {
final DisabledDuration lastOne;
if (!result.isEmpty()) {
lastOne = result.get(result.size() - 1);
} else {
lastOne = null;
}
final DateTime startDate = firstBlocking.getEffectiveDate();
final DateTime endDate = firstNonBlocking == null ? null : firstNonBlocking.getEffectiveDate();
if (lastOne != null && lastOne.getEnd().compareTo(startDate) == 0) {
lastOne.setEnd(endDate);
} else if (endDate == null || startDate.toLocalDate().compareTo(endDate.toLocalDate()) != 0) {
// Don't disable for periods less than a day (see https://github.com/killbill/killbill/issues/267)
result.add(new DisabledDuration(startDate, endDate));
}
}
@VisibleForTesting
static AtomicLong getGlobalTotalOrder() {
return globaltotalOrder;
}
}