use of org.killbill.billing.invoice.api.DryRunArguments in project killbill by killbill.
the class TestCatalogWithDryRun method testDryRunWithChangePlanWithOverridesOnSamePlan.
@Test(groups = "slow")
public void testDryRunWithChangePlanWithOverridesOnSamePlan() throws Exception {
final VersionedCatalog catalog = catalogUserApi.getCatalog("WeaponsHireSmall", callContext);
// Start with an initialDate such that: catalog V1=2020-09-16T10:34:25 < initialDate < catalog V2=2020-10-18T11:19:01
final LocalDate initialDate = new LocalDate(2020, 10, 1);
clock.setDay(initialDate);
final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(null));
final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("pistol-monthly", null);
busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
final UUID createdEntitlementId = entitlementApi.createBaseEntitlement(account.getId(), new DefaultEntitlementSpecifier(spec, null, null, null), UUID.randomUUID().toString(), null, null, false, true, ImmutableList.<PluginProperty>of(), callContext);
final Entitlement bpEntitlement = entitlementApi.getEntitlementForId(createdEntitlementId, callContext);
assertListenerStatus();
Invoice curInvoice = invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2020, 10, 1), null, InvoiceItemType.FIXED, new BigDecimal("100")), new ExpectedInvoiceItemCheck(new LocalDate(2020, 10, 1), new LocalDate(2020, 11, 1), InvoiceItemType.RECURRING, new BigDecimal("49.95")));
Assert.assertEquals(curInvoice.getInvoiceItems().get(0).getCatalogEffectiveDate().toDate().compareTo(catalog.getVersions().get(0).getEffectiveDate()), 0);
Assert.assertEquals(curInvoice.getInvoiceItems().get(1).getCatalogEffectiveDate().toDate().compareTo(catalog.getVersions().get(0).getEffectiveDate()), 0);
clock.setDay(new LocalDate(2020, 10, 19));
assertListenerStatus();
// Future change
final LocalDate changeDate = new LocalDate(2020, 10, 20);
final PlanPhaseSpecifier spec1 = new PlanPhaseSpecifier("Pistol", BillingPeriod.MONTHLY, "discount", null);
bpEntitlement.changePlanWithDate(new DefaultEntitlementSpecifier(spec1), changeDate, ImmutableList.<PluginProperty>of(), callContext);
assertListenerStatus();
// Generate the invoice
busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
invoiceUserApi.triggerInvoiceGeneration(bpEntitlement.getAccountId(), changeDate, callContext);
assertListenerStatus();
curInvoice = invoiceChecker.checkInvoice(account.getId(), 1, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2020, 10, 1), null, InvoiceItemType.FIXED, new BigDecimal("100")), new ExpectedInvoiceItemCheck(new LocalDate(2020, 10, 1), new LocalDate(2020, 11, 1), InvoiceItemType.RECURRING, new BigDecimal("49.95")));
Assert.assertEquals(curInvoice.getInvoiceItems().get(0).getCatalogEffectiveDate().toDate().compareTo(catalog.getVersions().get(0).getEffectiveDate()), 0);
Assert.assertEquals(curInvoice.getInvoiceItems().get(1).getCatalogEffectiveDate().toDate().compareTo(catalog.getVersions().get(0).getEffectiveDate()), 0);
curInvoice = invoiceChecker.checkInvoice(account.getId(), 2, callContext, new ExpectedInvoiceItemCheck(new LocalDate(2020, 10, 20), null, InvoiceItemType.FIXED, new BigDecimal("80")), new ExpectedInvoiceItemCheck(new LocalDate(2020, 10, 20), new LocalDate(2020, 11, 1), InvoiceItemType.RECURRING, new BigDecimal("7.72")), new ExpectedInvoiceItemCheck(new LocalDate(2020, 10, 20), new LocalDate(2020, 11, 1), InvoiceItemType.REPAIR_ADJ, new BigDecimal("-19.34")));
Assert.assertEquals(curInvoice.getInvoiceItems().get(0).getCatalogEffectiveDate().toDate().compareTo(catalog.getVersions().get(1).getEffectiveDate()), 0);
Assert.assertEquals(curInvoice.getInvoiceItems().get(1).getCatalogEffectiveDate().toDate().compareTo(catalog.getVersions().get(1).getEffectiveDate()), 0);
// Future dry-run change on the same plan with all prices overriden
final PlanPhasePriceOverride override = new DefaultPlanPhasePriceOverride("pistol-monthly-discount-evergreen", account.getCurrency(), new BigDecimal("70.00"), new BigDecimal("28.90"), ImmutableList.<UsagePriceOverride>of());
final List<PlanPhasePriceOverride> overrides = ImmutableList.<PlanPhasePriceOverride>of(override);
final DryRunArguments dryRunSubscriptionActionArg = new TestDryRunArguments(DryRunType.SUBSCRIPTION_ACTION, "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, "discount", PhaseType.EVERGREEN, SubscriptionEventType.CHANGE, bpEntitlement.getId(), bpEntitlement.getBundleId(), null, BillingActionPolicy.END_OF_TERM, overrides);
final LocalDate targetDate = new LocalDate(2020, 11, 1);
final Invoice dryRunInvoice = invoiceUserApi.triggerDryRunInvoiceGeneration(bpEntitlement.getAccountId(), targetDate, dryRunSubscriptionActionArg, callContext);
final List<ExpectedInvoiceItemCheck> expectedInvoices = new ArrayList<ExpectedInvoiceItemCheck>();
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2020, 11, 1), null, InvoiceItemType.FIXED, new BigDecimal("70.00")));
expectedInvoices.add(new ExpectedInvoiceItemCheck(new LocalDate(2020, 11, 1), new LocalDate(2020, 12, 1), InvoiceItemType.RECURRING, new BigDecimal("28.90")));
// Future dry-run change on the same plan with only the recuring price overriden
final PlanPhasePriceOverride override2 = new DefaultPlanPhasePriceOverride("pistol-monthly-discount-evergreen", account.getCurrency(), null, new BigDecimal("32.65"), ImmutableList.<UsagePriceOverride>of());
final List<PlanPhasePriceOverride> override2s = ImmutableList.<PlanPhasePriceOverride>of(override2);
final DryRunArguments dryRunSubscriptionActionArg2 = new TestDryRunArguments(DryRunType.SUBSCRIPTION_ACTION, "Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, "discount", PhaseType.EVERGREEN, SubscriptionEventType.CHANGE, bpEntitlement.getId(), bpEntitlement.getBundleId(), null, BillingActionPolicy.END_OF_TERM, override2s);
final Invoice dryRunInvoice2 = invoiceUserApi.triggerDryRunInvoiceGeneration(bpEntitlement.getAccountId(), targetDate, dryRunSubscriptionActionArg2, callContext);
final List<ExpectedInvoiceItemCheck> expectedInvoices2 = new ArrayList<ExpectedInvoiceItemCheck>();
expectedInvoices2.add(new ExpectedInvoiceItemCheck(new LocalDate(2020, 11, 1), null, InvoiceItemType.FIXED, new BigDecimal("80.00")));
expectedInvoices2.add(new ExpectedInvoiceItemCheck(new LocalDate(2020, 11, 1), new LocalDate(2020, 12, 1), InvoiceItemType.RECURRING, new BigDecimal("32.65")));
invoiceChecker.checkInvoiceNoAudits(dryRunInvoice2, expectedInvoices2);
}
use of org.killbill.billing.invoice.api.DryRunArguments in project killbill by killbill.
the class DefaultInternalBillingApi method addBillingEventsForBundles.
private void addBillingEventsForBundles(final ImmutableAccountData account, final DryRunArguments dryRunArguments, final InternalCallContext context, final DefaultBillingEventSet result, final Set<UUID> skipSubscriptionsSet, final Map<UUID, List<SubscriptionBase>> subscriptionsForAccount, final VersionedCatalog catalog, final List<Tag> tagsForAccount, final int currentAccountBCD) throws AccountApiException, CatalogApiException, SubscriptionBaseApiException {
// want to tap into subscriptionBase logic, so we make up a bundleId
if (dryRunArguments != null && dryRunArguments.getAction() == SubscriptionEventType.START_BILLING && dryRunArguments.getBundleId() == null) {
final UUID fakeBundleId = UUIDs.randomUUID();
final List<SubscriptionBase> subscriptions = subscriptionApi.getSubscriptionsForBundle(fakeBundleId, dryRunArguments, context);
addBillingEventsForSubscription(account, subscriptions, null, currentAccountBCD, context, result, skipSubscriptionsSet, catalog);
}
for (final UUID bundleId : subscriptionsForAccount.keySet()) {
final DryRunArguments dryRunArgumentsForBundle = (dryRunArguments != null && dryRunArguments.getBundleId() != null && dryRunArguments.getBundleId().equals(bundleId)) ? dryRunArguments : null;
final List<SubscriptionBase> subscriptions;
// In dryRun mode, optimization is intentionally left as is, since is not a common path.
if (dryRunArgumentsForBundle == null || dryRunArgumentsForBundle.getAction() == null) {
subscriptions = getSubscriptionsForAccountByBundleId(subscriptionsForAccount, bundleId);
} else {
subscriptions = subscriptionApi.getSubscriptionsForBundle(bundleId, dryRunArgumentsForBundle, context);
}
// Check if billing is off for the bundle
final List<Tag> bundleTags = getTagsForObjectType(ObjectType.BUNDLE, tagsForAccount, bundleId);
final boolean found_AUTO_INVOICING_OFF = is_AUTO_INVOICING_OFF(bundleTags);
if (found_AUTO_INVOICING_OFF) {
for (final SubscriptionBase subscription : subscriptions) {
// billing is off so list sub ids in set to be excluded
result.getSubscriptionIdsWithAutoInvoiceOff().add(subscription.getId());
}
} else {
// billing is not off
final SubscriptionBase baseSubscription = subscriptions != null && !subscriptions.isEmpty() ? subscriptions.get(0) : null;
addBillingEventsForSubscription(account, subscriptions, baseSubscription, currentAccountBCD, context, result, skipSubscriptionsSet, catalog);
}
}
// If dryRun is specified, we don't want to update the account BCD value, so we initialize the flag updatedAccountBCD to true
if (currentAccountBCD == 0) {
final Integer accountBCDCandidate = computeAccountBCD(result);
if (accountBCDCandidate == null) {
return;
}
// Because we now have computed the real BCD, we need to re-compute the BillingEvents BCD for ACCOUNT alignments (see BillCycleDayCalculator#calculateBcdForAlignment).
// The code could maybe be optimized (no need to re-run the full function?), but since it's run once per account, it's probably not worth it.
result.clear();
addBillingEventsForBundles(account, dryRunArguments, context, result, skipSubscriptionsSet, subscriptionsForAccount, catalog, tagsForAccount, accountBCDCandidate);
final boolean dryRunMode = dryRunArguments != null;
if (!dryRunMode) {
log.info("Setting account BCD='{}', accountId='{}'", accountBCDCandidate, account.getId());
accountApi.updateBCD(account.getExternalKey(), accountBCDCandidate, context);
}
}
}
use of org.killbill.billing.invoice.api.DryRunArguments in project killbill by killbill.
the class InvoiceResource method generateDryRunInvoice.
@TimedResource
@POST
@Path("/" + DRY_RUN)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Generate a dryRun invoice", response = InvoiceJson.class)
@ApiResponses(value = { /* Already added by default */
@ApiResponse(code = 204, message = "Nothing to generate"), @ApiResponse(code = 400, message = "Invalid account id or target datetime supplied") })
public Response generateDryRunInvoice(@Nullable final InvoiceDryRunJson dryRunSubscriptionSpec, @ApiParam(required = true) @QueryParam(QUERY_ACCOUNT_ID) final UUID accountId, @Nullable @QueryParam(QUERY_TARGET_DATE) final String targetDate, @HeaderParam(HDR_CREATED_BY) final String createdBy, @HeaderParam(HDR_REASON) final String reason, @HeaderParam(HDR_COMMENT) final String comment, @javax.ws.rs.core.Context final HttpServletRequest request, @javax.ws.rs.core.Context final UriInfo uriInfo) throws AccountApiException, InvoiceApiException {
final CallContext callContext = context.createCallContextWithAccountId(accountId, createdBy, reason, comment, request);
final LocalDate inputDate = (dryRunSubscriptionSpec != null && DryRunType.UPCOMING_INVOICE.equals(dryRunSubscriptionSpec.getDryRunType())) ? null : toLocalDate(targetDate);
// On the other hand if body is not null, we are attempting a dryRun subscription operation
if (dryRunSubscriptionSpec != null && dryRunSubscriptionSpec.getDryRunAction() != null) {
if (SubscriptionEventType.START_BILLING.equals(dryRunSubscriptionSpec.getDryRunAction())) {
verifyNonNullOrEmpty(dryRunSubscriptionSpec.getProductName(), "DryRun subscription product category should be specified");
verifyNonNullOrEmpty(dryRunSubscriptionSpec.getBillingPeriod(), "DryRun subscription billingPeriod should be specified");
verifyNonNullOrEmpty(dryRunSubscriptionSpec.getProductCategory(), "DryRun subscription product category should be specified");
if (dryRunSubscriptionSpec.getProductCategory().equals(ProductCategory.ADD_ON)) {
verifyNonNullOrEmpty(dryRunSubscriptionSpec.getBundleId(), "DryRun bundleID should be specified");
}
} else if (SubscriptionEventType.CHANGE.equals(dryRunSubscriptionSpec.getDryRunAction())) {
verifyNonNullOrEmpty(dryRunSubscriptionSpec.getProductName(), "DryRun subscription product category should be specified");
verifyNonNullOrEmpty(dryRunSubscriptionSpec.getBillingPeriod(), "DryRun subscription billingPeriod should be specified");
verifyNonNullOrEmpty(dryRunSubscriptionSpec.getSubscriptionId(), "DryRun subscriptionID should be specified");
verifyNonNullOrEmpty(dryRunSubscriptionSpec.getBundleId(), "DryRun bundleID should be specified");
} else if (SubscriptionEventType.STOP_BILLING.equals(dryRunSubscriptionSpec.getDryRunAction())) {
verifyNonNullOrEmpty(dryRunSubscriptionSpec.getSubscriptionId(), "DryRun subscriptionID should be specified");
verifyNonNullOrEmpty(dryRunSubscriptionSpec.getBundleId(), "DryRun bundleID should be specified");
}
}
final Account account = accountUserApi.getAccountById(accountId, callContext);
final DryRunArguments dryRunArguments = new DefaultDryRunArguments(dryRunSubscriptionSpec, account);
try {
final Invoice generatedInvoice = invoiceApi.triggerDryRunInvoiceGeneration(accountId, inputDate, dryRunArguments, callContext);
return Response.status(Status.OK).entity(new InvoiceJson(generatedInvoice, null, null)).build();
} catch (InvoiceApiException e) {
if (e.getCode() == ErrorCode.INVOICE_NOTHING_TO_DO.getCode()) {
return Response.status(Status.NO_CONTENT).build();
}
throw e;
}
}
use of org.killbill.billing.invoice.api.DryRunArguments in project killbill by killbill.
the class TestUserApiChangePlan method testChangePlanOnPendingSubscription.
@Test(groups = "slow")
public void testChangePlanOnPendingSubscription() throws SubscriptionBaseApiException {
final String baseProduct = "Shotgun";
final BillingPeriod baseTerm = BillingPeriod.MONTHLY;
final String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
final LocalDate startDate = clock.getUTCToday().plusDays(5);
final DefaultSubscriptionBase subscription = testUtil.createSubscription(bundle, baseProduct, baseTerm, basePriceList, startDate);
assertEquals(subscription.getState(), Entitlement.EntitlementState.PENDING);
assertEquals(subscription.getStartDate().compareTo(startDate.toDateTime(accountData.getReferenceTime())), 0);
final PlanPhaseSpecifier planPhaseSpecifier = new PlanPhaseSpecifier("Pistol", baseTerm, basePriceList);
final EntitlementSpecifier spec = new DefaultEntitlementSpecifier(planPhaseSpecifier, null, null, null);
// First try with default api (no date -> IMM) => Call should fail because subscription is PENDING
final DryRunArguments dryRunArguments1 = testUtil.createDryRunArguments(subscription.getId(), subscription.getBundleId(), spec, null, SubscriptionEventType.CHANGE, null);
final List<SubscriptionBase> result1 = subscriptionInternalApi.getSubscriptionsForBundle(subscription.getBundleId(), dryRunArguments1, internalCallContext);
// Check we are seeing the right PENDING transition (pistol-monthly), not the START but the CHANGE on the same date
assertEquals(((DefaultSubscriptionBase) result1.get(0)).getCurrentOrPendingPlan().getName(), "pistol-monthly");
assertEquals(((DefaultSubscriptionBase) result1.get(0)).getPendingTransition().getTransitionType(), SubscriptionBaseTransitionType.CREATE);
// Second try with date prior to startDate => Call should fail because subscription is PENDING
try {
final DryRunArguments dryRunArguments2 = testUtil.createDryRunArguments(subscription.getId(), subscription.getBundleId(), spec, startDate.minusDays(1), SubscriptionEventType.CHANGE, null);
subscriptionInternalApi.getSubscriptionsForBundle(subscription.getBundleId(), dryRunArguments2, internalCallContext);
fail("Change plan should have failed : subscription PENDING");
} catch (final SubscriptionBaseApiException e) {
assertEquals(e.getCode(), ErrorCode.SUB_CHANGE_NON_ACTIVE.getCode());
}
try {
subscription.changePlanWithDate(spec, subscription.getStartDate().minusDays(1), callContext);
fail("Change plan should have failed : subscription PENDING");
} catch (final SubscriptionBaseApiException e) {
assertEquals(e.getCode(), ErrorCode.SUB_INVALID_REQUESTED_DATE.getCode());
}
// Third try with date equals to startDate Call should succeed, but no event because action in future
final DryRunArguments dryRunArguments3 = testUtil.createDryRunArguments(subscription.getId(), subscription.getBundleId(), spec, startDate, SubscriptionEventType.CHANGE, null);
final List<SubscriptionBase> result2 = subscriptionInternalApi.getSubscriptionsForBundle(subscription.getBundleId(), dryRunArguments3, internalCallContext);
// Check we are seeing the right PENDING transition (pistol-monthly), not the START but the CHANGE on the same date
assertEquals(((DefaultSubscriptionBase) result2.get(0)).getCurrentOrPendingPlan().getName(), "pistol-monthly");
subscription.changePlanWithDate(spec, subscription.getStartDate(), callContext);
assertListenerStatus();
// Move clock to startDate
testListener.pushExpectedEvents(NextEvent.CREATE);
clock.addDays(5);
assertListenerStatus();
final DefaultSubscriptionBase subscription2 = (DefaultSubscriptionBase) subscriptionInternalApi.getSubscriptionFromId(subscription.getId(), internalCallContext);
assertEquals(subscription2.getStartDate().compareTo(subscription.getStartDate()), 0);
assertEquals(subscription2.getState(), Entitlement.EntitlementState.ACTIVE);
assertEquals(subscription2.getCurrentPlan().getProduct().getName(), "Pistol");
// Same original # active events
assertEquals(subscription2.getEvents().size(), subscription.getEvents().size());
}
use of org.killbill.billing.invoice.api.DryRunArguments in project killbill by killbill.
the class DefaultInternalBillingApi method addBillingEventsForBundles.
private void addBillingEventsForBundles(final List<SubscriptionBaseBundle> bundles, final ImmutableAccountData account, final DryRunArguments dryRunArguments, final InternalCallContext context, final DefaultBillingEventSet result, final Set<UUID> skipSubscriptionsSet) throws AccountApiException, CatalogApiException, SubscriptionBaseApiException {
final boolean dryRunMode = dryRunArguments != null;
// want to tap into subscriptionBase logic, so we make up a bundleId
if (dryRunArguments != null && dryRunArguments.getAction() == SubscriptionEventType.START_BILLING && dryRunArguments.getBundleId() == null) {
final UUID fakeBundleId = UUIDs.randomUUID();
final List<SubscriptionBase> subscriptions = subscriptionApi.getSubscriptionsForBundle(fakeBundleId, dryRunArguments, context);
addBillingEventsForSubscription(account, subscriptions, null, dryRunMode, context, result, skipSubscriptionsSet);
}
for (final SubscriptionBaseBundle bundle : bundles) {
final DryRunArguments dryRunArgumentsForBundle = (dryRunArguments != null && dryRunArguments.getBundleId() != null && dryRunArguments.getBundleId().equals(bundle.getId())) ? dryRunArguments : null;
final List<SubscriptionBase> subscriptions = subscriptionApi.getSubscriptionsForBundle(bundle.getId(), dryRunArgumentsForBundle, context);
//Check if billing is off for the bundle
final List<Tag> bundleTags = tagApi.getTags(bundle.getId(), ObjectType.BUNDLE, context);
boolean found_AUTO_INVOICING_OFF = is_AUTO_INVOICING_OFF(bundleTags);
if (found_AUTO_INVOICING_OFF) {
for (final SubscriptionBase subscription : subscriptions) {
// billing is off so list sub ids in set to be excluded
result.getSubscriptionIdsWithAutoInvoiceOff().add(subscription.getId());
}
} else {
// billing is not off
final SubscriptionBase baseSubscription = !subscriptions.isEmpty() ? subscriptions.get(0) : null;
addBillingEventsForSubscription(account, subscriptions, baseSubscription, dryRunMode, context, result, skipSubscriptionsSet);
}
}
}
Aggregations