diff options
Diffstat (limited to 'controller-server')
8 files changed, 282 insertions, 47 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BcpGroupUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BcpGroupUpdater.java index 1ad4feb1897..36aeb598890 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BcpGroupUpdater.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BcpGroupUpdater.java @@ -71,6 +71,24 @@ public class BcpGroupUpdater extends ControllerMaintainer { var patch = new ApplicationPatch(); addTrafficShare(deployment, bcpGroups, patch); addBcpGroupInfo(deployment.zone().region(), metrics.get(instance.id()), bcpGroups, patch); + + StringBuilder patchAsStringBuilder = new StringBuilder("Patch of instance ").append(instance.id().serializedForm()).append(": ") + .append("\n\tcurrentReadShare: ") + .append(patch.currentReadShare) + .append("\n\tmaxReadShare: ") + .append(patch.maxReadShare); + for (Map.Entry<String, ApplicationPatch.ClusterPatch> entry : patch.clusters.entrySet()) { + String key = entry.getKey(); + ApplicationPatch.ClusterPatch value = entry.getValue(); + patchAsStringBuilder.append("\n\tbcpGroupInfo for ").append(key).append(": ") + .append("\n\t\tcpuCostPerQuery: ") + .append(value.bcpGroupInfo.cpuCostPerQuery) + .append("\n\t\tqueryRate: ") + .append(value.bcpGroupInfo.queryRate) + .append("\n\t\tgrowthRateHeadroom: ") + .append(value.bcpGroupInfo.growthRateHeadroom); + } + log.log(Level.FINER, patchAsStringBuilder.toString()); nodeRepository.patchApplication(deployment.zone(), instance.id(), patch); } catch (Exception e) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java index 9121c139b00..55428e80493 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java @@ -121,32 +121,22 @@ public class CloudTrialExpirer extends ControllerMaintainer { // Ignore tenants that are on a paid plan and skip from inclusion in updated data structure } else if (status == null && "trial".equals(plan) && ageInDays <= 1) { updatedStatus.add(updatedStatus(tenant, now, SIGNED_UP)); - queueNotification(tenant, "Welcome to Vespa Cloud", "Welcome to Vespa Cloud", - "Welcome to Vespa Cloud! We hope you will enjoy your trial. " + - "Please reach out to us if you have any questions or feedback."); + notifySignup(tenant); } else if ("none".equals(plan) && !List.of(EXPIRED).contains(state)) { updatedStatus.add(updatedStatus(tenant, now, EXPIRED)); - queueNotification(tenant, "Your Vespa Cloud trial has expired", "Your Vespa Cloud trial has expired", - "Your Vespa Cloud trial has expired. " + - "Please reach out to us if you have any questions or feedback."); + notifyExpired(tenant); } else if ("trial".equals(plan) && ageInDays >= 13 && !List.of(EXPIRES_IMMEDIATELY, EXPIRED).contains(state)) { updatedStatus.add(updatedStatus(tenant, now, EXPIRES_IMMEDIATELY)); - queueNotification(tenant, "Your Vespa Cloud trial expires tomorrow", "Your Vespa Cloud trial expires tomorrow", - "Your Vespa Cloud trial expires tomorrow. " + - "Please reach out to us if you have any questions or feedback."); + notifyExpiresImmediately(tenant); } else if ("trial".equals(plan) && ageInDays >= 12 && !List.of(EXPIRES_SOON, EXPIRES_IMMEDIATELY, EXPIRED).contains(state)) { updatedStatus.add(updatedStatus(tenant, now, EXPIRES_SOON)); - queueNotification(tenant, "Your Vespa Cloud trial expires in 2 days", "Your Vespa Cloud trial expires in 2 days", - "Your Vespa Cloud trial expires in 2 days. " + - "Please reach out to us if you have any questions or feedback."); + notifyExpiresSoon(tenant); } else if ("trial".equals(plan) && ageInDays >= 7 && !List.of(MID_CHECK_IN, EXPIRES_SOON, EXPIRES_IMMEDIATELY, EXPIRED).contains(state)) { updatedStatus.add(updatedStatus(tenant, now, MID_CHECK_IN)); - queueNotification(tenant, "How is your Vespa Cloud trial going?", "How is your Vespa Cloud trial going?", - "How is your Vespa Cloud trial going? " + - "Please reach out to us if you have any questions or feedback."); + notifyMidCheckIn(tenant); } else { updatedStatus.add(status); } @@ -160,6 +150,41 @@ public class CloudTrialExpirer extends ControllerMaintainer { } } + private void notifySignup(Tenant tenant) { + var consoleMsg = "Welcome to Vespa Cloud trial! [Manage plan](%s)".formatted(billingUrl(tenant)); + queueNotification(tenant, consoleMsg, "Welcome to Vespa Cloud", + "Welcome to Vespa Cloud! We hope you will enjoy your trial. " + + "Please reach out to us if you have any questions or feedback."); + } + + private void notifyMidCheckIn(Tenant tenant) { + var consoleMsg = "You're halfway through the **14 day** trial period. [Manage plan](%s)".formatted(billingUrl(tenant)); + queueNotification(tenant, consoleMsg, "How is your Vespa Cloud trial going?", + "How is your Vespa Cloud trial going? " + + "Please reach out to us if you have any questions or feedback."); + } + + private void notifyExpiresSoon(Tenant tenant) { + var consoleMsg = "Your Vespa Cloud trial expires in **2** days. [Manage plan](%s)".formatted(billingUrl(tenant)); + queueNotification(tenant, consoleMsg, "Your Vespa Cloud trial expires in 2 days", + "Your Vespa Cloud trial expires in 2 days. " + + "Please reach out to us if you have any questions or feedback."); + } + + private void notifyExpiresImmediately(Tenant tenant) { + var consoleMsg = "Your Vespa Cloud trial expires **tomorrow**. [Manage plan](%s)".formatted(billingUrl(tenant)); + queueNotification(tenant, consoleMsg, "Your Vespa Cloud trial expires tomorrow", + "Your Vespa Cloud trial expires tomorrow. " + + "Please reach out to us if you have any questions or feedback."); + } + + private void notifyExpired(Tenant tenant) { + var consoleMsg = "Your Vespa Cloud trial has expired. [Upgrade plan](%s)".formatted(billingUrl(tenant)); + queueNotification(tenant, consoleMsg, "Your Vespa Cloud trial has expired", + "Your Vespa Cloud trial has expired. " + + "Please reach out to us if you have any questions or feedback."); + } + private void queueNotification(Tenant tenant, String consoleMsg, String emailSubject, String emailMsg) { var mail = Optional.of(Notification.MailContent.fromTemplate(MailTemplating.Template.DEFAULT_MAIL_CONTENT) .subject(emailSubject) @@ -175,6 +200,8 @@ public class CloudTrialExpirer extends ControllerMaintainer { source, Notification.Type.account, Notification.Level.info, consoleMsg, List.of(), mail); } + private String billingUrl(Tenant t) { return controller().serviceRegistry().consoleUrls().tenantBilling(t.name()); } + private static TrialNotifications.Status updatedStatus(Tenant t, Instant i, TrialNotifications.State s) { return new TrialNotifications.Status(t.name(), s, i); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java index b0b43866fae..f27e69c4636 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java @@ -101,10 +101,13 @@ public class Notifier { log.fine(() -> "Sending notification " + notification + " to " + contacts.stream().map(c -> c.email().getEmailAddress()).toList()); var content = formatter.format(notification); - mailer.send(mailOf(content, contacts.stream() - .filter(c -> c.email().isVerified()) - .map(c -> c.email().getEmailAddress()) - .toList())); + var verifiedContacts = contacts.stream() + .filter(c -> c.email().isVerified()).map(c -> c.email().getEmailAddress()).toList(); + if (verifiedContacts.isEmpty()) { + log.fine(() -> "None of the %d contact(s) are verified - skipping delivery of %s".formatted(contacts.size(), notification)); + return; + } + mailer.send(mailOf(content, verifiedContacts)); } catch (MailerException e) { log.log(Level.SEVERE, "Failed sending email", e); } catch (MissingOptionalException e) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java index d5bb47c94b0..eae8f86f289 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java @@ -28,6 +28,8 @@ import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant; import com.yahoo.vespa.hosted.controller.tenant.Email; import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo; +import com.yahoo.vespa.hosted.controller.tenant.PurchaseOrder; +import com.yahoo.vespa.hosted.controller.tenant.TaxId; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.vespa.hosted.controller.tenant.TenantAddress; import com.yahoo.vespa.hosted.controller.tenant.TenantBilling; @@ -93,6 +95,9 @@ public class TenantSerializer { private static final String cloudAccountsField = "cloudAccounts"; private static final String accountField = "account"; private static final String templateVersionField = "templateVersion"; + private static final String taxIdField = "taxId"; + private static final String purchaseOrderField = "purchaseOrder"; + private static final String invoiceEmailField = "invoiceEmail"; private static final String awsIdField = "awsId"; private static final String roleField = "role"; @@ -282,12 +287,19 @@ public class TenantSerializer { } private TenantBilling tenantInfoBillingContactFromSlime(Inspector billingObject) { + var taxId = new TaxId(billingObject.field(taxIdField).asString()); + var purchaseOrder = new PurchaseOrder(billingObject.field(purchaseOrderField).asString()); + var invoiceEmail = new Email(billingObject.field(invoiceEmailField).asString(), false); + return TenantBilling.empty() .withContact(TenantContact.from( billingObject.field("name").asString(), new Email(billingObject.field("email").asString(), billingObject.field("emailVerified").asBool()), billingObject.field("phone").asString())) - .withAddress(tenantInfoAddressFromSlime(billingObject.field("address"))); + .withAddress(tenantInfoAddressFromSlime(billingObject.field("address"))) + .withTaxId(taxId) + .withPurchaseOrder(purchaseOrder) + .withInvoiceEmail(invoiceEmail); } private List<TenantSecretStore> secretStoresFromSlime(Inspector secretStoresObject) { @@ -349,6 +361,9 @@ public class TenantSerializer { billingCursor.setString("email", billingContact.contact().email().getEmailAddress()); billingCursor.setBool("emailVerified", billingContact.contact().email().isVerified()); billingCursor.setString("phone", billingContact.contact().phone()); + billingCursor.setString(taxIdField, billingContact.getTaxId().value()); + billingCursor.setString(purchaseOrderField, billingContact.getPurchaseOrder().value()); + billingCursor.setString(invoiceEmailField, billingContact.getInvoiceEmail().getEmailAddress()); toSlime(billingContact.address(), billingCursor); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index 6a6c8a51d72..5548928b9d0 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -127,6 +127,8 @@ import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant; import com.yahoo.vespa.hosted.controller.tenant.Email; import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo; import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification; +import com.yahoo.vespa.hosted.controller.tenant.PurchaseOrder; +import com.yahoo.vespa.hosted.controller.tenant.TaxId; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.vespa.hosted.controller.tenant.TenantAddress; import com.yahoo.vespa.hosted.controller.tenant.TenantBilling; @@ -694,6 +696,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { contact.setString("email", billingContact.contact().email().getEmailAddress()); contact.setBool("emailVerified", billingContact.contact().email().isVerified()); contact.setString("phone", billingContact.contact().phone()); + root.setString("taxId", billingContact.getTaxId().value()); + root.setString("purchaseOrder", billingContact.getPurchaseOrder().value()); + root.setString("invoiceEmail", billingContact.getInvoiceEmail().getEmailAddress()); toSlime(billingContact.address(), root); // will create "address" on the parent } @@ -703,15 +708,22 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { private SlimeJsonResponse putTenantInfoBilling(CloudTenant cloudTenant, Inspector inspector) { var info = cloudTenant.info(); - var contact = info.billingContact().contact(); - var address = info.billingContact().address(); + var billing = info.billingContact(); + var contact = billing.contact(); + var address = billing.address(); var mergedContact = updateBillingContact(inspector.field("contact"), cloudTenant.name(), contact); - var mergedAddress = updateTenantInfoAddress(inspector.field("address"), info.billingContact().address()); + var mergedAddress = updateTenantInfoAddress(inspector.field("address"), billing.address()); + var mergedTaxId = optional("taxId", inspector).map(TaxId::new).orElse(billing.getTaxId()); + var mergedPurchaseOrder = optional("purchaseOrder", inspector).map(PurchaseOrder::new).orElse(billing.getPurchaseOrder()); + var mergedInvoiceEmail = optional("invoiceEmail", inspector).map(mail -> new Email(mail, false)).orElse(billing.getInvoiceEmail()); var mergedBilling = info.billingContact() .withContact(mergedContact) - .withAddress(mergedAddress); + .withAddress(mergedAddress) + .withTaxId(mergedTaxId) + .withPurchaseOrder(mergedPurchaseOrder) + .withInvoiceEmail(mergedInvoiceEmail); var mergedInfo = info.withBilling(mergedBilling); @@ -764,6 +776,11 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { throw new IllegalArgumentException("'website' needs to be a valid address"); } } + if (! mergedInfo.billingContact().getInvoiceEmail().isBlank()) { + // TODO: Validate invoice email is set if collection method is INVOICE + if (! mergedInfo.billingContact().getInvoiceEmail().getEmailAddress().contains("@")) + throw new IllegalArgumentException("'Invoice email' needs to be an email address"); + } } private void toSlime(TenantAddress address, Cursor parentCursor) { @@ -785,6 +802,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { billingCursor.setString("email", billingContact.contact().email().getEmailAddress()); billingCursor.setBool("emailVerified", billingContact.contact().email().isVerified()); billingCursor.setString("phone", billingContact.contact().phone()); + billingCursor.setString("taxId", billingContact.getTaxId().value()); + billingCursor.setString("purchaseOrder", billingContact.getPurchaseOrder().value()); + billingCursor.setString("invoiceEmail", billingContact.getInvoiceEmail().getEmailAddress()); toSlime(billingContact.address(), billingCursor); } @@ -914,9 +934,15 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { private TenantBilling updateTenantInfoBillingContact(Inspector insp, TenantName tenantName, TenantBilling oldContact) { if (!insp.valid()) return oldContact; + var taxId = optional("taxId", insp).map(TaxId::new).orElse(oldContact.getTaxId()); + var purchaseOrder = optional("purchaseOrder", insp).map(PurchaseOrder::new).orElse(oldContact.getPurchaseOrder()); + var invoiceEmail = optional("invoiceEmail", insp).map(mail -> new Email(mail, false)).orElse(oldContact.getInvoiceEmail()); return TenantBilling.empty() .withContact(updateBillingContact(insp, tenantName, oldContact.contact())) - .withAddress(updateTenantInfoAddress(insp.field("address"), oldContact.address())); + .withAddress(updateTenantInfoAddress(insp.field("address"), oldContact.address())) + .withTaxId(taxId) + .withPurchaseOrder(purchaseOrder) + .withInvoiceEmail(invoiceEmail); } private TenantContacts updateTenantInfoContacts(Inspector insp, TenantName tenantName, TenantContacts oldContacts) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java index 07ce2e415a7..4056459c532 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java @@ -110,27 +110,32 @@ public class CloudTrialExpirerTest { .withBooleanFlag(Flags.CLOUD_TRIAL_NOTIFICATIONS.id(), true); registerTenant(tenant.value(), "trial", Duration.ZERO); assertEquals(0.0, expirer.maintain()); - assertEquals("Welcome to Vespa Cloud", lastAccountLevelNotificationTitle(tenant)); + var expected = "Welcome to Vespa Cloud trial! [Manage plan](https://console.tld/tenant/trial-tenant/account/billing)"; + assertEquals(expected, lastAccountLevelNotificationTitle(tenant)); assertLastEmailEquals(mailer, "welcome.html"); + expected = "You're halfway through the **14 day** trial period. [Manage plan](https://console.tld/tenant/trial-tenant/account/billing)"; clock.advance(Duration.ofDays(7)); assertEquals(0.0, expirer.maintain()); - assertEquals("How is your Vespa Cloud trial going?", lastAccountLevelNotificationTitle(tenant)); + assertEquals(expected, lastAccountLevelNotificationTitle(tenant)); assertLastEmailEquals(mailer, "trial-reminder.html"); + expected = "Your Vespa Cloud trial expires in **2** days. [Manage plan](https://console.tld/tenant/trial-tenant/account/billing)"; clock.advance(Duration.ofDays(5)); assertEquals(0.0, expirer.maintain()); - assertEquals("Your Vespa Cloud trial expires in 2 days", lastAccountLevelNotificationTitle(tenant)); + assertEquals(expected, lastAccountLevelNotificationTitle(tenant)); assertLastEmailEquals(mailer, "trial-expiring-soon.html"); + expected = "Your Vespa Cloud trial expires **tomorrow**. [Manage plan](https://console.tld/tenant/trial-tenant/account/billing)"; clock.advance(Duration.ofDays(1)); assertEquals(0.0, expirer.maintain()); - assertEquals("Your Vespa Cloud trial expires tomorrow", lastAccountLevelNotificationTitle(tenant)); + assertEquals(expected, lastAccountLevelNotificationTitle(tenant)); assertLastEmailEquals(mailer, "trial-expiring-immediately.html"); + expected = "Your Vespa Cloud trial has expired. [Upgrade plan](https://console.tld/tenant/trial-tenant/account/billing)"; clock.advance(Duration.ofDays(2)); assertEquals(0.0, expirer.maintain()); - assertEquals("Your Vespa Cloud trial has expired", lastAccountLevelNotificationTitle(tenant)); + assertEquals(expected, lastAccountLevelNotificationTitle(tenant)); assertLastEmailEquals(mailer, "trial-expired.html"); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java index 9de856971c9..4912c9ae407 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java @@ -24,6 +24,8 @@ import com.yahoo.vespa.hosted.controller.tenant.CloudTenant; import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant; import com.yahoo.vespa.hosted.controller.tenant.Email; import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo; +import com.yahoo.vespa.hosted.controller.tenant.PurchaseOrder; +import com.yahoo.vespa.hosted.controller.tenant.TaxId; import com.yahoo.vespa.hosted.controller.tenant.TenantAddress; import com.yahoo.vespa.hosted.controller.tenant.TenantBilling; import com.yahoo.vespa.hosted.controller.tenant.TenantContact; @@ -234,7 +236,11 @@ public class TenantSerializerTest { .withCity("Suddery") .withCountry("Sodor") .withAddress("Central Station") - .withRegion("Irish Sea"))); + .withRegion("Irish Sea")) + .withPurchaseOrder(new PurchaseOrder("PO42")) + .withTaxId(new TaxId("1234L")) + .withInvoiceEmail(new Email("billing@mycomp.any", false)) + ); Slime slime = new Slime(); Cursor parentCursor = slime.setObject(); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java index 2b01d87c903..32f0247b3bc 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java @@ -98,19 +98,68 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest { @Test void tenant_info_billing() { + var expectedResponse = """ + { + "contact": { + "name":"", + "email":"", + "emailVerified":false, + "phone":"" + }, + "taxId":"", + "purchaseOrder":"", + "invoiceEmail":"" + } + """; var request = request("/application/v4/tenant/scoober/info/billing", GET) .roles(Set.of(Role.reader(tenantName))); - tester.assertResponse(request, "{\"contact\":{\"name\":\"\",\"email\":\"\",\"emailVerified\":false,\"phone\":\"\"}}", 200); - - var fullAddress = "{\"addressLines\":\"addressLines\",\"postalCodeOrZip\":\"postalCodeOrZip\",\"city\":\"city\",\"stateRegionProvince\":\"stateRegionProvince\",\"country\":\"country\"}"; - var fullBillingContact = "{\"contact\":{\"name\":\"name\",\"email\":\"foo@example\",\"phone\":\"phone\"},\"address\":" + fullAddress + "}"; - + tester.assertJsonResponse(request, expectedResponse, 200); + + var fullBillingContact = """ + { + "contact": { + "name":"name", + "email":"foo@example", + "phone":"phone" + }, + "taxId":"1234L", + "purchaseOrder":"PO9001", + "invoiceEmail":"billing@mycomp.any", + "address": { + "addressLines":"addressLines", + "postalCodeOrZip":"postalCodeOrZip", + "city":"city", + "stateRegionProvince":"stateRegionProvince", + "country":"country" + } + } + """; var updateRequest = request("/application/v4/tenant/scoober/info/billing", PUT) .data(fullBillingContact) .roles(Set.of(Role.administrator(tenantName))); tester.assertResponse(updateRequest, "{\"message\":\"Tenant info updated\"}", 200); - tester.assertResponse(request, "{\"contact\":{\"name\":\"name\",\"email\":\"foo@example\",\"emailVerified\":false,\"phone\":\"phone\"},\"address\":{\"addressLines\":\"addressLines\",\"postalCodeOrZip\":\"postalCodeOrZip\",\"city\":\"city\",\"stateRegionProvince\":\"stateRegionProvince\",\"country\":\"country\"}}", 200); + expectedResponse = """ + { + "contact": { + "name":"name", + "email":"foo@example", + "emailVerified": false, + "phone":"phone" + }, + "taxId":"1234L", + "purchaseOrder":"PO9001", + "invoiceEmail":"billing@mycomp.any", + "address": { + "addressLines":"addressLines", + "postalCodeOrZip":"postalCodeOrZip", + "city":"city", + "stateRegionProvince":"stateRegionProvince", + "country":"country" + } + } + """; + tester.assertJsonResponse(request, expectedResponse, 200); } @Test @@ -120,12 +169,32 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest { tester.assertResponse(request, "{\"contacts\":[{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"developer@scoober\",\"emailVerified\":true}]}", 200); - var fullContacts = "{\"contacts\":[{\"audiences\":[\"tenant\"],\"email\":\"contact1@example.com\",\"emailVerified\":false},{\"audiences\":[\"notifications\"],\"email\":\"contact2@example.com\",\"emailVerified\":false},{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"contact3@example.com\",\"emailVerified\":false}]}"; + var fullContacts = """ + { + "contacts":[ + { + "audiences":["tenant"] + ,"email":"contact1@example.com", + "emailVerified":false + }, + { + "audiences":["notifications"], + "email":"contact2@example.com", + "emailVerified":false + }, + { + "audiences":["tenant","notifications"], + "email":"contact3@example.com", + "emailVerified":false + } + ] + } + """; var updateRequest = request("/application/v4/tenant/scoober/info/contacts", PUT) .data(fullContacts) .roles(Set.of(Role.administrator(tenantName))); tester.assertResponse(updateRequest, "{\"message\":\"Tenant info updated\"}", 200); - tester.assertResponse(request, fullContacts, 200); + tester.assertJsonResponse(request, fullContacts, 200); } @Test @@ -150,13 +219,79 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest { tester.assertResponse(postPartialContacts, "{\"message\":\"Tenant info updated\"}", 200); // Read back the updated info - tester.assertResponse(infoRequest, "{\"name\":\"\",\"email\":\"\",\"website\":\"\",\"contactName\":\"newName\",\"contactEmail\":\"foo@example.com\",\"contactEmailVerified\":false,\"billingContact\":{\"name\":\"billingName\",\"email\":\"\",\"emailVerified\":false,\"phone\":\"\"},\"contacts\":[{\"audiences\":[\"tenant\"],\"email\":\"contact1@example.com\",\"emailVerified\":false}]}", 200); - - String fullAddress = "{\"addressLines\":\"addressLines\",\"postalCodeOrZip\":\"postalCodeOrZip\",\"city\":\"city\",\"stateRegionProvince\":\"stateRegionProvince\",\"country\":\"country\"}"; - String fullBillingContact = "{\"name\":\"name\",\"email\":\"foo@example\",\"emailVerified\":false,\"phone\":\"phone\",\"address\":" + fullAddress + "}"; - String fullContacts = "[{\"audiences\":[\"tenant\"],\"email\":\"contact1@example.com\",\"emailVerified\":false},{\"audiences\":[\"notifications\"],\"email\":\"contact2@example.com\",\"emailVerified\":false},{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"contact3@example.com\",\"emailVerified\":false}]"; - String fullInfo = "{\"name\":\"name\",\"email\":\"foo@example\",\"website\":\"https://yahoo.com\",\"contactName\":\"contactName\",\"contactEmail\":\"contact@example.com\",\"contactEmailVerified\":false,\"address\":" + fullAddress + ",\"billingContact\":" + fullBillingContact + ",\"contacts\":" + fullContacts + "}"; - + var expectedResponse = """ + { + "name":"", + "email":"", + "website":"", + "contactName":"newName", + "contactEmail":"foo@example.com", + "contactEmailVerified":false, + "billingContact": { + "name":"billingName", + "email":"","emailVerified":false, + "phone":"", + "taxId":"", + "purchaseOrder":"", + "invoiceEmail":"" + }, + "contacts": [ + {"audiences":["tenant"],"email":"contact1@example.com","emailVerified":false} + ] + } + """; + tester.assertJsonResponse(infoRequest, expectedResponse, 200); + + var fullInfo = """ + { + "name":"name", + "email":"foo@example", + "website":"https://yahoo.com", + "contactName":"contactName", + "contactEmail":"contact@example.com", + "contactEmailVerified":false, + "address": { + "addressLines":"addressLines", + "postalCodeOrZip":"postalCodeOrZip", + "city":"city", + "stateRegionProvince":"stateRegionProvince", + "country":"country" + }, + "billingContact": { + "name":"name", + "email":"foo@example", + "emailVerified":false, + "phone":"phone", + "taxId":"", + "purchaseOrder":"", + "invoiceEmail":"", + "address": { + "addressLines":"addressLines", + "postalCodeOrZip":"postalCodeOrZip", + "city":"city", + "stateRegionProvince":"stateRegionProvince", + "country":"country" + } + }, + "contacts": [ + { + "audiences":["tenant"], + "email":"contact1@example.com", + "emailVerified":false + }, + { + "audiences":["notifications"], + "email":"contact2@example.com", + "emailVerified":false + }, + { + "audiences":["tenant","notifications"] + ,"email":"contact3@example.com", + "emailVerified":false + } + ] + } + """; // Now set all fields var postFull = request("/application/v4/tenant/scoober/info", PUT) @@ -165,7 +300,7 @@ public class ApplicationApiCloudTest extends ControllerContainerCloudTest { tester.assertResponse(postFull, "{\"message\":\"Tenant info updated\"}", 200); // Now compare the updated info with the full info we sent - tester.assertResponse(infoRequest, fullInfo, 200); + tester.assertJsonResponse(infoRequest, fullInfo, 200); var invalidBody = "{\"mail\":\"contact1@example.com\", \"mailType\":\"blurb\"}"; var resendMailRequest = |