/* * Copyright 2010-2013 Ning, Inc. * Copyright 2014-2016 Groupon, Inc * Copyright 2014-2016 The Billing Project, LLC * * The Billing Project 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.subscription.engine.core; import java.util.UUID; import org.joda.time.DateTime; import org.killbill.billing.callcontext.InternalCallContext; import org.killbill.billing.catalog.api.CatalogApiException; import org.killbill.billing.catalog.api.ProductCategory; import org.killbill.billing.platform.api.LifecycleHandlerType; import org.killbill.billing.platform.api.LifecycleHandlerType.LifecycleLevel; import org.killbill.billing.subscription.alignment.PlanAligner; import org.killbill.billing.subscription.alignment.TimedPhase; import org.killbill.billing.subscription.api.SubscriptionBaseApiService; import org.killbill.billing.subscription.api.SubscriptionBaseService; import org.killbill.billing.subscription.api.user.DefaultEffectiveSubscriptionEvent; import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase; import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData; import org.killbill.billing.subscription.engine.dao.SubscriptionDao; import org.killbill.billing.subscription.events.SubscriptionBaseEvent; import org.killbill.billing.subscription.events.SubscriptionBaseEvent.EventType; import org.killbill.billing.subscription.events.phase.PhaseEvent; import org.killbill.billing.subscription.events.phase.PhaseEventData; import org.killbill.billing.subscription.exceptions.SubscriptionBaseError; import org.killbill.billing.util.callcontext.CallContext; import org.killbill.billing.util.callcontext.CallOrigin; import org.killbill.billing.util.callcontext.InternalCallContextFactory; import org.killbill.billing.util.callcontext.UserType; import org.killbill.bus.api.BusEvent; import org.killbill.bus.api.PersistentBus; import org.killbill.bus.api.PersistentBus.EventBusException; import org.killbill.clock.Clock; import org.killbill.notificationq.api.NotificationEvent; import org.killbill.notificationq.api.NotificationQueue; import org.killbill.notificationq.api.NotificationQueueService; import org.killbill.notificationq.api.NotificationQueueService.NoSuchNotificationQueue; import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueAlreadyExists; import org.killbill.notificationq.api.NotificationQueueService.NotificationQueueHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; public class DefaultSubscriptionBaseService implements EventListener, SubscriptionBaseService { public static final String NOTIFICATION_QUEUE_NAME = "subscription-events"; public static final String SUBSCRIPTION_SERVICE_NAME = "subscription-service"; private static final Logger log = LoggerFactory.getLogger(DefaultSubscriptionBaseService.class); private final Clock clock; private final SubscriptionDao dao; private final PlanAligner planAligner; private final PersistentBus eventBus; private final NotificationQueueService notificationQueueService; private final InternalCallContextFactory internalCallContextFactory; private final SubscriptionBaseApiService apiService; private NotificationQueue subscriptionEventQueue; @Inject public DefaultSubscriptionBaseService(final Clock clock, final SubscriptionDao dao, final PlanAligner planAligner, final PersistentBus eventBus, final NotificationQueueService notificationQueueService, final InternalCallContextFactory internalCallContextFactory, final SubscriptionBaseApiService apiService) { this.clock = clock; this.dao = dao; this.planAligner = planAligner; this.eventBus = eventBus; this.notificationQueueService = notificationQueueService; this.internalCallContextFactory = internalCallContextFactory; this.apiService = apiService; } @Override public String getName() { return SUBSCRIPTION_SERVICE_NAME; } @LifecycleHandlerType(LifecycleLevel.INIT_SERVICE) public void initialize() { try { final NotificationQueueHandler queueHandler = new NotificationQueueHandler() { @Override public void handleReadyNotification(final NotificationEvent inputKey, final DateTime eventDateTime, final UUID fromNotificationQueueUserToken, final Long accountRecordId, final Long tenantRecordId) { if (!(inputKey instanceof SubscriptionNotificationKey)) { log.error("SubscriptionBase service received an unexpected event className='{}'", inputKey.getClass().getName()); return; } final SubscriptionNotificationKey key = (SubscriptionNotificationKey) inputKey; final SubscriptionBaseEvent event = dao.getEventById(key.getEventId(), internalCallContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId)); if (event == null) { // This can be expected if the event is soft deleted (is_active = 0) log.debug("Failed to extract event for notification key {}", inputKey); return; } final InternalCallContext context = internalCallContextFactory.createInternalCallContext(tenantRecordId, accountRecordId, "SubscriptionEventQueue", CallOrigin.INTERNAL, UserType.SYSTEM, fromNotificationQueueUserToken); processEventReady(event, key.getSeqId(), context); } }; subscriptionEventQueue = notificationQueueService.createNotificationQueue(SUBSCRIPTION_SERVICE_NAME, NOTIFICATION_QUEUE_NAME, queueHandler); } catch (final NotificationQueueAlreadyExists e) { throw new RuntimeException(e); } } @LifecycleHandlerType(LifecycleLevel.START_SERVICE) public void start() { subscriptionEventQueue.startQueue(); } @LifecycleHandlerType(LifecycleLevel.STOP_SERVICE) public void stop() throws NoSuchNotificationQueue { if (subscriptionEventQueue != null) { subscriptionEventQueue.stopQueue(); notificationQueueService.deleteNotificationQueue(subscriptionEventQueue.getServiceName(), subscriptionEventQueue.getQueueName()); } } @Override public void processEventReady(final SubscriptionBaseEvent event, final int seqId, final InternalCallContext context) { if (!event.isActive()) { return; } try { final DefaultSubscriptionBase subscription = (DefaultSubscriptionBase) dao.getSubscriptionFromId(event.getSubscriptionId(), context); if (subscription == null) { log.warn("Error retrieving subscriptionId='{}'", event.getSubscriptionId()); return; } final SubscriptionBaseTransitionData transition = subscription.getTransitionFromEvent(event, seqId); if (transition == null) { log.warn("Skipping event ='{}', no matching transition was built", event.getType()); return; } boolean eventSent = false; if (event.getType() == EventType.PHASE) { eventSent = onPhaseEvent(subscription, event, context); } else if (event.getType() == EventType.API_USER && subscription.getCategory() == ProductCategory.BASE) { final CallContext callContext = internalCallContextFactory.createCallContext(context); eventSent = onBasePlanEvent(subscription, event, callContext); } else if (event.getType() == EventType.BCD_UPDATE) { eventSent = false; } if (!eventSent) { // Methods above invoking the DAO will send this event directly from the transaction final BusEvent busEvent = new DefaultEffectiveSubscriptionEvent(transition, subscription.getAlignStartDate(), context.getUserToken(), context.getAccountRecordId(), context.getTenantRecordId()); eventBus.post(busEvent); } } catch (final EventBusException e) { log.warn("Failed to post event {}", event, e); } catch (final CatalogApiException e) { log.warn("Failed to post event {}", event, e); } } private boolean onPhaseEvent(final DefaultSubscriptionBase subscription, final SubscriptionBaseEvent readyPhaseEvent, final InternalCallContext context) { try { final TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription, readyPhaseEvent.getEffectiveDate(), context); final PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ? PhaseEventData.createNextPhaseEvent(subscription.getId(), nextTimedPhase.getPhase().getName(), nextTimedPhase.getStartPhase()) : null; if (nextPhaseEvent != null) { dao.createNextPhaseEvent(subscription, readyPhaseEvent, nextPhaseEvent, context); return true; } } catch (final SubscriptionBaseError e) { log.warn("Error inserting next phase for subscriptionId='{}'", subscription.getId(), e); } return false; } private boolean onBasePlanEvent(final DefaultSubscriptionBase baseSubscription, final SubscriptionBaseEvent event, final CallContext context) throws CatalogApiException { apiService.handleBasePlanEvent(baseSubscription, event, context); return true; } }